Repository: microsoft/terminal (language: cpp)

Issue:
Scenario: Windows Terminal 2.0 Process Model Improvements
This scenario is tracking a number of enhancements we'd like to make to the fundamental process model of the Windows Terminal in v2.0. The following deliverables all have something to do with the way the Terminal is launched, or the way connections or instances are managed, and as such, the solution to any one of these areas should make sure to take into account the others.

## Window / Session Managment
* [x]  #1256 **Tab Tearoff/ Reattach**
  - #2080 is a draft spec for tearoff and default terminal app (below)
  - Presumably this will involve some way to communicate different terminal instances from one window to another.
    - Proposed ideas include a manager process that's hosting all the terminal instances and connections, and then separate windows for actually drawing these instances on the screen
* [x] **Run `wt` in the current window**
  - See #4472.
  - This is being included because it ties strongly with the above deliverable.
  - See #3495 / [#607 - Commandline Arguments for the Windows Terminal.md](https://github.com/microsoft/terminal/blob/master/doc/specs/%23607%20-%20Commandline%20Arguments%20for%20the%20Windows%20Terminal.md#future-considerations), which also brought this idea up.
  - This involves finding some way for the user to specify on the commandline "I want to perform [some command] in [the current window | a specified other WT window | a new WT window]"
  - ✔️ Implemented in #8898
* [x] **Single Instance Mode**
  - See #2227
  - This would be an important parallel work thread for #653 and #492 
  - ✔️ Implemented in #9118
* [x] #653 **Quake-style dropdown**
  - Again, included in this list for it's fundamental strong ties to session management.
  - Often mentioned in the discussion of quake-style dropdown is the need for a single-session mode, so pressing the global hotkey would always activate the single instance, and new windows would instead glom to the existing instance
  - See #8888 for follow-up tasks related to Quake mode

## Elevation
* [x] #1032 **Mixed Elevation**
  - This is a _hard_ problem, and I'm making no firm commitments that we'll be able to solve it for sure in 2.0.
  - The goal of this deliverable is to find a safe way that we can support both elevated (High-IL) and unelevated (Medium-IL) tabs, panes, etc all in the same window.
  - Ideally, if a user creates a new _elevated_ tab from an unelevated window, then all the unelevated tabs would seamlessly appear in a _new_ elevated  HWND hosting both unelevated and elevated tabs simultaneously.
  - Elevated connections can't be hosted in an unelevated HWND, because that allows for a trivial escalation-of-privilege vector utilizing the Terminal.
  - **This problem should arguably be investigated first**. If this is something that's technically possible, it will certainly have an influence on the way the rest of these deliverables are architected. 
  - ❌ **Resolution**: This is not going to be technically achievable, unfortunately. 
* [x] #632 **Open a Profile as elevated**
  - If the above is not achievable technically, then the user should still be able to specify a way to force a profile to open elevated.
  - If mixed elevation isn't possible:
    - If the current window isn't currently elevated, open a new window when someone's profile is marked "elevated: true"
    - If the current window _is_ elevated, open in the same window.

## Default Terminal
* [x] #492 **Default Terminal Application**
  - This one's a little different than the others. While the rest were Windows Terminal specific features, this deliverable is more of a OS feature than a Terminal feature.
  - This concerns the ability for an arbitrary application to be registered with the OS as the "Default Terminal Application", and be started automatically when the user starts an arbitrary commandline application.
  - This also has some interaction with the above deliverables. When a commandline application is started this way, should they open as a new tab in an existing Terminal window, or start a new window?


# Specs

* Spec for Windows Terminal Process Model 2.0 #7240
* Spec for Windows Terminal Window Management #8135
* Spec for Elevation QOL improvements #8455
* Default Terminal Spec #7414
* Quake mode spec #9274 

## TODOs

These are also tracked in https://github.com/microsoft/terminal/projects/5. This provides a nested list, to mentally track which things are dependent upon other things. This is basically a copy of a page of my notebook. 

<details>
<summary>Window Management</summary>

* [x] Window Management, PR: #8898
  * [ ] Next/Prev window keybindings
  * [x] Windowing Behavior, PR: #9118 
    * [ ] Smart `wt -w 0` handling
    * [x] Name windows on commandline 
      * [x] `nameWindow` action (#9662)
      * [x] `openWindowRenamer` action (#9662)
      * [x] window-level toast for displaying window ID, action: `idenfifyWindows` (#9523)
        - Originally I thought I'd need generic toasts for this, but I'm gonna just do this first, and come back to generic toasts
* [x] Quake Mode has been moved to #8888
* [x] New Window Action PR: #9208 

</details>


### Tear Out
Currently planned state, notes:

See https://github.com/microsoft/terminal/issues/5000#issuecomment-1407110045 for the most up-to-date todo list. There were some dramatic changes to our plans in early 2023, which basically entirely obsoleted "Process Model 2" (where each termcontrol is its own process) in favor of "Process Model 3" (where _all_ windows are all one process). 


<details>
<summary>Original tear-out plans</summary>


* [ ] `wt.exe` accepts `--content guid` on the commandline to start in content mode. Additionally, Make the `Control` able to have the `Core` OOP.
  * these two were originally different bullet points, but I think they need to be combined into one singular PR. They don't make sense alone. 
  - [x] in content process mode, it registers `ContentProcess` for that GUID with `CoRegisterClassObject`
  - [x] `ContentProcess::Initialize(IControlSettings, IConnection)` instantiates a new `ControlInteractivity` (in the content process) (if one hasn't already been initialized)
  - [x] `ContentProcess::RequestSwapChainHandle(pid)` will duplicate the handle if the pid is different
  - [x] `wt` properly tracks the lifetime of its content. When that's discarded by the last window process, we'll exit.
  - [x] Do the above line, but better. Now we're using a lock and an event, which is _ew_. Instead, we should do the Conhost `ExitThread()` thing on the main thread, and then ExitProcess() when the content process is dtor'd.
  - [x] Move all the chaos I've introduced in `main.cpp` into its own dedicated file/class or something. 
  - [x] An embedding process needs a way to know that the content process is ready, better than a `Sleep(2000)`
    - A handle to an event is probably the cleanest way - have the window create the event, then pass the handle in `CreateProcess` & on the commandline to `wt --content {guid} --signal {handle}`, then have the content process set the event when it's ready.
  - 📝 in ~`dev/migrie/oop/the-whole-thing`~ `dev/migrie/oop/infinity-war`
  - [x] The `Connection` is made in the core
  - [x] When the Core signals that the swapchain changed, and the TermControl asks what the swapchain handle is, the Control passes in it's PID. If the PID isn't the same, `DuplicateHandle` the swapchain to that PID. 
  - [x] The sample is updated with a version of the Control that's also OOP
  - [x] If the content process dies, the control needs to be able to display a message box. 
  - [x] Make sure UIA signaling still works (`accevent.exe` in the SDK, `C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64`)
  - [x] Make sure that reading the contents of the buffer with Accessibility Insights still works
    - 💩 
    - `InteractivityAutomationPeer` extends `Windows.UI.Xaml.Automation.Peers.AutomationPeer`. But we're making the interactivity peer in the content process, not in the control layer. 
    - When we construct the `IAP`, we throw instantly, because the `WUX..AutomationPeer` can't be created off the main thread. 
    - How do we only expose the `WUX..AutomationPeer` via `TermControl`, but not `IAP`?
  - [x] The `_raiseContentDied` should display the error internal to the TermControl. Kinda like the renderer warning, but for this, we need to kill the `SwapChainPanel` too.
  - in ~`dev/migrie/oop/the-whole-thing`~ `dev/migrie/oop/infinity-war`
  - Relevant diff: 23a19c58181b489b10a2049232f23b033dbdab8d...5c17603a94a76d47c4427f49493bca4eeb8fa774
  - [x] Add a velocity flag to gate behind dev builds only. Probably safe to just gate the `--content` flag for now.
  - [ ] Guard calls to `_interactivity`, `_core` in `TermControl`. Those objects might go out of scope at any moment, so pretty much all access needs to be try/caught.
    - we may want to do a function-level try, with a CATCH_RAISE_DIALOG() macro we write to raise the connection died notice. Might be the most minimal diff. 
    - Could likely be a follow up to original PR. 
    - There's a silly idea in `dev/migrie/oop/2/cptn-marvel`. I really hate it, it's a HUGE amount of churn.
  - [x] **There's a lot of debug spew** that makes me think the OOP core does something off the main thread.
    - BriAl and I synced - this is nothing to worry about. This is basically just COM logging "i'm about to increment the refcount" 
    - Still trying to find a viable way to suppress that logging. 
  - [x] What the hell did I do with the automation peer. Was what was in `main` correct? Revisit the original PRs for inspiration.
    - Did it ever work? there's seemingly no record of it working in 23a19c58181b489b10a2049232f23b033dbdab8d...5c17603a94a76d47c4427f49493bca4eeb8fa774
    - #10051 has a fairly different implementation than the above diff
    - I may have simply forgotten that narrator / UIA uses exceptions for control flow, which makes debugging a pain
  - [x] `ControlCore::_IsClosing` TODO!
  - [x] `ContentProcess::RequestSwapChainHandle` tracelogging TODO!
  - [x] `TermControlAutomationPeer::GetLocalizedControlTypeCore` that string used to come from localized resources, it can't anymore.
  - [x] The `Control` properly manages the lifetime of the `HANDLE` duplicated to it. A `wil::unique_handle`?
  - [x] **IMPORTANT**: For whatever reason, when the second window attaches to the content, it renders basically nothing. Input gets sent just fine, but the output doesn't render.. Fixed in 7b3ca8332
  - 📝 Now moved to [`dev/migrie/oop/2/infinity-war`](https://github.com/microsoft/terminal/compare/dev/migrie/oop/2/infinity-war)
  - 👀 in #12938
* [x] `TermControl`s need to have a local `TerminalSettings` instance. (Related: #7219)
  - What a shockingly simple oversight. We did all this work to get the connections in the same proc as the `TermControl`, we totally forgot the Settings.
  - [x] Make sure updating/changing settings works fine across processes
  - [x] Make sure previewing the color scheme still works. That works with some parent/child trickery that won't work OOP.
  - 📝 in `dev/migrie/oop/ragnarok`
* [ ] Some misc cleanup before endgame:
  - [ ] `existingConnection` in `NewTab` was redundant / unused
  - [ ] `_SplitPane` renamed to `_SplitPaneActiveTab` etc.
  - [ ] Refactoring to allow the TerminalTab to be initialized without a control
  - [ ] Other stuff to, to minimize the diff
  - not started, in `dev/migrie/oop/2/black-widow` cause this one's not really consequential, is it now. 
* [ ] Make the app able to do the OOP instantiation.
  - Notes in https://github.com/microsoft/terminal/issues/5000#issuecomment-1137653858
* [x] Make the sample able to pop a control into a new window.
  - ✔️ in `dev/migrie/oop/the-whole-thing`. I didn't pop it into a new window, just made it so multiple windows could connect to the same one. 
* [ ] Defterm is out of proc.
  - [ ] DEFTERM: Incoming connections need to be tossed to the `ContentProcess`. Guh.
  - not started in `dev/migrie/oop/2/captain-falcon`. 
* [x] Tabs can be serialized to a json blob, with their tab & pane structure. 
  - See #766, because this is highly relevant
  - This may have already been done for me by #10972 
  - [x] Serialize `TerminalTab`s
  - [x] Serialize `SettingsTab`s
* [ ] `move-pane -w <id>` to move a pane/tab to another Terminal window. This will prove we can move panes without the whole instantiation of a new window process.
  - Notes in https://github.com/microsoft/terminal/issues/5000#issuecomment-1183694124
  - 📝 in [`dev/migrie/oop/wandavision`](https://github.com/microsoft/terminal/compare/81a5a736fef41bd90339c36a6cc4d7a339b6badf...dev/migrie/oop/wandavision) (link to relevant diff from `dev/migrie/oop/endgame`)
* [ ] When a tab is torn out of a Terminal window, it can be dropped & re-attached onto another terminal window
  - Notes in https://github.com/microsoft/terminal/issues/5000#issuecomment-1185447944
  - 📝 in `dev/migrie/oop/2/loki`
* [ ] When a tab is torn out of a Terminal window, it can be dropped somewhere else and turned into a new terminal window
  - not started yet, in `dev/migrie/oop/no-way-home`
* [ ] When a tab is torn out, we'll create a new window for the tabs left behind, and move the current window around. 
  - We'll need to still be able to drop this window on an existing window somehow.
  - Use EvanK's prototype here. We're gonna need to extend the drag bar over the TabView and do everything ourselves
  - [ ] #12616
  - not started yet, in `dev/migrie/oop/multiverse-of-madness`



</details>

#### other notes
Just moving these lower in the body to make managing this doc easier

<details>

<summary>This is a pseudo design that Dustin and I discussed. There's a lot more work that needs to be done here, but I need to save this somewhere outside of a Teams chat history
</summary>


```
---
struct TerminalControl: winrt::implements<MUX::IUIElement> {} // XAML Control
// calls: interactivity.PointerMoved(Point{x, y});
// calls: interactivity.PointerClicked(Point{x, y});
// links: TerminalCore.lib <----> or consumes TerminalCore.dll from RT
^^^ TerminalControl.dll ^^^

---
struct WPFTerminalControl { HWND _h; }
// calls: interactivity.PointerMoved(Point{x, y});
// calls: interactivity.PointerClicked(Point{x, y});
// links: TerminalCore.lib
^^^ WPFTerminalControl.dll ^^^

vvv TerminalCore.lib vvv // Cancelled. TerminalCore.lib or .dll
struct Coord {};
struct PointerInfo {};
struct KeyInfo {};
struct TerminalControlInteractivity;
struct ControlCore; // < implements ... something? // this should just be TerminalCore?
struct TerminalCore; // < implements the "conhosty APIs (ITerminalDispatch, etc.)
// links: dx.lib buffer.lib types.lib

vvv TerminalCore.UnitTests.dll vvv
// contains: interactivity tests
// contains: core tests
// links: TerminalCore.lib
```

* [x] Split `TerminalControl` into lib and dll
* [x] Split `TermControl` into `Control.TermControl` and `Control.ControlCore` and `Control.Interactivity`
* [ ] Add some `Core.ControlCore` tests from things in #7001
* [ ] Create a WinRT boundary on `Core.Interactivity` between it and `TermControl`
* [ ] Enable `Core.ControlCore` to exist out-of-proc with XAML, or in-proc
  * This might involve creating a scratch project to consume an in-proc one and an out-of-proc one 
* [ ] the Core can create connections, or have them passed in. (so an OOP core can have a connection in it's own process)
* [ ] Much more. This is just what's immediately on my radar.


</details>

<details>
<summary>_4-30-21_: </summary>
I need to do the DX `HANDLE` thing before I can do the rest of the projection boundary. The projection boundary is in `dev/migrie/interactivity-projection-000`, which is almost done now.

`dev/migrie/oop-scratch-4` has the final state of the OOP prototyping on this element. In that branch, 
* `HostAndProcess::CreateSwapChain` creates a swapchain with `DCompositionCreateSurfaceHandle`. 
* It attaches it to the `SwapChainPanel` with `ISwapChainPanelNative2::SetSwapChainHandle`.
* It duplicates that handle to the content process, and calls `HostClass::ThisIsInsane`
* Later on, the `HostClass` passes that handle to the `DxEngine`

So we'll need to do basically that, but a little different, to handle all the actual edge cases we have.

</details>

<details>
<summary>pre-1.10 era work</summary>

* [x] First, update `DxEngine` to accept the handle, but don't otherwise change anything (#10023)
  * ~~Then, change `TermControl` to create the handle with `DCompositionCreateSurfaceHandle`, hook it up to the `SwapChainPanel` with `ISwapChainPanelNative2::SetSwapChainHandle`.~~
    - `DxEngine::_CreateDeviceResources` will need to trigger a callback in the `Core`, that will then ask the `TermControl` to create a new swapchain for it. 
    - Wait no that's wrong. the `DxEngine` will always be making the swapchains, because it knows when it needs a new one.
  * [x] `DxEngine` will always make new swapchains, when needed, with `DCompositionCreateSurfaceHandle`. It'll raise an event when that happens. The `Core` will handle that, then raise an event the control will listen to. The `Control` will handle that event, then ask the `Core` for the new swapchain `HANDLE`, as a `uint64_t`. The Control will use that `uint64_t` to attach to the `SwapChainPanel` via `ISwapChainPanelNative2::SetSwapChainHandle`.
    - This is also the way it is in (#10023)
  - 👀 in #10023
* [x] Then we'll come back to the rest of the projection
  <!-- - ✔️ in `dev/migrie/interactivity-projection-000`. -->
  - 👀 in #10051
* [x] At this point, we should make a sample app that's just a grid with two spaces in it. One for an in-proc term control, and one for an out of proc one. We won't do the OOP one yet.
  <!-- - ✔️ in `dev/migrie/oop/sample-project`-->
  - 👀 in #10067
* [x] Merge #10115 into #10067
* [x] Enable the Connection able to be created OOP from the rest of the TerminalPage. The content process will be the one to instantiate the connection.
  - [x] The Core is passed a (guid? String?) of a type of connection to spawn, and an `IConnectionSettings` object. The Core instantiates a type of connection based on that guid/string, and passes the `IConnectionSettings` from the window process to that new constructor.
  - [x] How does the azure connection work? I think that one's implemented in TerminalApp. If it's a String, we could pass the winrt name, `ITerminalConnection conn = RoCreateInstance("TerminalApp.AzureConnection"); conn.Create(iConnectionSettings);` , right?
    - No, turns out the Azure connection is in TerminalConnection. So this was actually fine the whole time. 
    - Whatever, we're keeping the `RoGetActivationFactory` thing though, for future needs.
  - [ ] How does the debug tap work?
  - ~~Switch from `RoActivateInstance`&`Initialize` to `RoGetActivationFactory`~~
    - > Even neglecting the Windows Runtime, this isn’t even possible in C++ or C#. 
  - [x] Reattach event handlers that I disabled for performance's sake
  - [x] Merge #10115 into this branch, or into `dev/migrie/oop/connection-factory-rebase`, which is attempting to rebase these changes onto `dev/migrie/oop/sample-project`
  - 📝 in [`dev/migrie/oop/connection-factory`](https://github.com/microsoft/terminal/compare/dev/migrie/oop/the-whole-thing...dev/migrie/oop/connection-factory)
* [x] 🦶 **PUNTED** ~Move the Core, the Interactivity, into `Core.dll`. We'll need that for:~
  - We may not actually need this. It'd be _nice_ to not have to load _all_ the settings, but we could live with it.

</details>







Discussion / hints:
> ## Elevation
> * [x]  #1032 **Mixed Elevation**
>   
>   * This is a _hard_ problem, and I'm making no firm commitments that we'll be able to solve it for sure in 2.0.
>   * The goal of this deliverable is to find a safe way that we can support both elevated (High-IL) and unelevated (Medium-IL) tabs, panes, etc all in the same window.
>   * Ideally, if a user creates a new _elevated_ tab from an unelevated window, then all the unelevated tabs would seamlessly appear in a _new_ elevated  HWND hosting both unelevated and elevated tabs simultaneously.
>   * Elevated connections can't be hosted in an unelevated HWND, because that allows for a trivial escalation-of-privilege vector utilizing the Terminal.
>   * **This problem should arguably be investigated first**. If this is something that's technically possible, it will certainly have an influence on the way the rest of these deliverables are architected.
>   * ❌ **Resolution**: This is not going to be technically achievable, unfortunately.
> * [ ]  #632 **Open a Profile as elevated**
>   
>   * If the above is not achievable technically, then the user should still be able to specify a way to force a profile to open elevated.
>   * If mixed elevation isn't possible:
>     
>     * If the current window isn't currently elevated, open a new window when someone's profile is marked "elevated: true"
>     * If the current window _is_ elevated, open in the same window.
> 

What does this mean re. https://github.com/microsoft/terminal/issues/6893#issuecomment-657554241 and https://github.com/microsoft/terminal/issues/3581#issuecomment-581624314 etc.?

Will each window belong to a different process? Otherwise I don't understand what does "elevated window" even means.

And what if "elevation" actually mean "OTS elevation" rather than "AAM elevation", since the non-elevated user is not an admin? Currently at least that works. I hope the new model won't somehow break that.

@DHowett-MSFT @miniksa 

Currently, yes, each terminal window is its own process. Each WT process has one window, so when you're running WT elevated, the window itself is elevated. Unfortunately, nothing in the new process model is going to have an affect on elevated drag drop. The team that owns the platform drag/drop APIs has specifically told us that drag/drop is unsupported in an elevated HWND. 

Furthermore, we're not going to be messing with mixed elevation as a part of this effort anymore, because that turned out to be technically less possible than we hoped. So nothing about that story is about to change. 
Maybe I'm missing something, but I just opened two elevated instances of WordPad, and dragged some text, a picture and an OLE Paint drawing from one to the other. And inside each document.

If by "platform" you mean UWP/Store/XAML/whatever-you-call-it-today, I just launched WinDbg Preview, installed from the Store, uses XAML and all that jazz, and managed to rearrange the windows in the elevated instance by dragging.
Specifically: "UWP XAML" (not WPF, which WinDbg Preview uses) uses the modern WinRT drag/drop APIs. Those APIs require a data broker process. That broker just doesn't work from an elevated context. Terminal, by way of it using "UWP XAML", is subject to this specific limitation.
About the **Mixed Elevation** part: Is there anything preventing possibility of a feature to launch medium integrity shells from high integrity wt instance? I know this is rather limited in comparison to the original plans, however should be much easier to support
> to launch medium integrity shells from high integrity wt instance

I would like to hear what others think about this approach. This can be achieved running `gsudo -i medium` on a wt tab or by creating a profile for it. There is also MediumPlus Integrity, which is higher than Medium (no medium process can tamper with it) but still hasn't admin acesss... (`gsudo -i MediumPlus`)  
Just as something that I saw mentioned as a comment on the spec, but not incorporated, I think the idea of having a separated `elevated-settings.json` file makes a lot of sense as a counterpart to `elevanted-state.json`. This gives a number of benefits
- The file can be admin writeable only to help prevent malicious modification
- You get for "free" admin specific settings modifications, including but not limited to custom styling, admin only profiles, and admin-specific startup actions.


> separated `elevated-settings.json` file makes a lot of sense as a counterpart to `elevanted-state.json`. 

I don't think this is a terrible idea. How do we deal with users who are currently using their unelevated settings in admin windows though? Would users need to maintain basically two copies of their settings file?

Maybe we only use the `elevated-settings` file if we find it? If the file doesn't exist, then just fall back to the normal settings.

I guess it might make more sense if the `elevated-settings` could be layered on top of `settings.json` (a la #2303, #6502, #2933)
I wasn't thinking in terms of migration as much as qol, but I did also consider layering as an approach or adding a "create from unelevated settings" option in the settings UI. I didn't think thoroughly about the security implications (neither are technically worse than what exists now, but it could be better) so I didn't want to suggest something.
I'd say use settings.json as a base and override/merge values with anything in the elevated.json. Changed values or added sessions.
i agree, layering / overriding is the best choice, in either direction.
although checking if elevated-settings.json exists, then using that and overriding with user one might make more sense so that 

> The file can be admin writeable only to help prevent malicious modification

is not pointless
\<notes to self>

### 📝 `dev/migrie/oop/2/endgame`

📝 in `dev/migrie/oop/2/endgame`
Relevant diff (probably): 5c17603a94a76d47c4427f49493bca4eeb8fa774...81a5a736fef41bd90339c36a6cc4d7a339b6badf
  
`_MakePane` usage:
* [x] AppActionHandlers.cpp:207 - `_HandleSplitPane` 
  - always creates a new split via `_SplitPane`  (_`_SplitPaneActiveTab`_)
* [x] TabManagement.cpp:88 - `_OpenNewTab`
  - always hits up `_CreateNewTabFromPane`
  - Actually, literally used in a `_CreateNewTabFromPane` call. Pane is not used afterwards.
* [x] TabManagement.cpp:351 - `_DuplicateTab`
  - always hits up `_CreateNewTabFromPane`
  - DOES USE THE TAB afterwards. Should be possible to just init the TerminalTab, do the copying of the title to the new tab, then connect the content later. 
* [x] TabManagement.cpp:374 - `_SplitTab`
  - used in a call to `_SplitPane` (_`_SplitPaneOnTab`_)
  - This is used in the tab context menu for SplitTab. Can't just use an action, cause the tab context is lost then. 
* [x] TerminalPage.cpp:1032 - `_OpenNewTerminalViaDropdown`
  - Either creates a new tab (`_CreateNewTabFromPane`) or a new pane (`_SplitPane`,  (_`_SplitPaneActiveTab`_)), based on alt key state. 
  - ⭐ THIS ONE IS THE SPECIAL ONE
  - **All the others IMMEDIATELY use the pane in `_SplitPane` or `_CreateNewTabFromPane`**. This is the only one that uses it in post. 
  - This could be two separate calls to `AsyncSplitPane` or `AsyncCreateNewTabFromPane` or something like that.
* [ ] TerminalPage.cpp:3519 - `_OnNewConnection`
  - always hits up `_CreateNewTabFromPane`
  - This is the incoming defterm connection, which... needs a lot more work
  
  
#### 2022-06-22

<details>
<summary>No this is dumb.</summary>

* `TerminalPage::_SplitPane` and `TerminalPage::_CreateNewTabFromPane` both take a `shared_ptr<Pane>`, created from `_MakePane`.
* Could they be trivially replaced by async versions?
  * `NewTabFromPane`
    * `resume_bg`
    * await `MakePane().get()`
      * create content process
      * resume_fg
      * create control from content
      * create pane
    * 
    
</details>

 

<details>
Make content is async. Creating the pane can be sync on the main thread
  * `IAsyncAction<ContentProcess> _CreateContentProcess(profile, controlSettings)`
  * `std::shared_ptr<Pane> _MakePaneFromContent(ContentProcess content, auto controlSettings, auto profile)`
	* // MUST BE ON FG THREAD
    * create control from content
    * create pane
  * `_CreateNewTabFromPane` becomes: `winrt::fire_and_forget TerminalPage::_AsyncCreateNewTab(IAsyncAction<ContentProcess> initContentProc)`
    * `resume_bg`
    * `auto content = initContentProc.get()`
    * `resume_fg`
    * `auto pane = _MakePaneFromContent()`
    * `tab = make_self<TerminalTab>(pane)`
    * `_InitializeTab(newTabImpl)`
    * And this is called like: `_AsyncCreateNewTab(_CreateContentProcess(profile, controlSettings));`
  * `_SplitPaneOnTab` becomes:  `winrt::fire_and_forget TerminalPage::_AsyncSplitPaneOnTab(TerminalTab& tab, const SplitDirection splitDirection, const float splitSize, IAsyncAction<ContentProcess> initContentProc)`
    * calculate split type
    * unzoom
    * resume BG
    * `auto content = initContentProc.get()`
    * `resume_fg`
    * `auto newPane = _MakePaneFromContent()`
    * `tab.SplitPane(*realSplitType, splitSize, newPane);`
    * focus the thing
  * `_SplitPaneActiveTab` becomes:
    * if (!focusedTab) ...
      * `_AsyncCreateNewTab`
    * else
      * `_AsyncSplitPaneOnTab(focusedTab,...)`

</details>

#### 2022-06-23

* eb4b2a753
  * We probably need to be able to make tabs without a pane, so we can put something on the screen before the content was connected.
* 19605bf75
  * Changed so that the tab could be created without a pane. This should make things a bit more seamless, the tab will appear and then content will fill the tab when it's ready to appear
  * Defterm should be easy. Every defterm connection automatically becomes a new content process. Each defterm content process then needs to ask the monarch for a window to attach to. EZ.
  
#### TODO
  - [ ] How does the debugTap work for this? Connections live in their content process, so the debug tap would have to get created when the content process itself is connected
    - Could we _always_ create debug taps for connections, then have a keybinding to _activate_ them? Or would that add a terrible lot of overhead. Probably. But a neat wackadoodle idea.
  - [x] Make sure pane splitting still works
  - [ ] DEFTERM: Can we just add the defterm tab in-proc, alongside other oop tabs?
  - [x] Add a setting to disable this functionality.
    - [x] Works for new tabs via dropdown
    - [x] new splits
    - [x] new tabs via action
    - [x] ‼️ startup actions / state restore
  - [x] Add velocity, disabled-by-default, enabled for dev builds. 
  - [x] Is there any way to get the tab opened _faster_, while we wait for the content to actually start? Even if we're going to just fake it?
  - [ ] Add a dummy BG to the tab, while we wait for the content to load
  - [x] Make sure moving panes between tabs still works
  - [x] init the content proc with the focused/unfocused appearance pair correctly
  - [x] There's a deadlock in conpty when exiting the Terminal as of 2ab0a50a1. is this the Terminal not releasing the connection cleanly? I'd reckon. There's no windowsterminal.exe processes left in VS when this happens. They're both locked on `RundownAndExit`
    - [I think this is fixed as of 83649075a71fd3ffb82cad578204fd8eae95d91c, dea94d51ade3f8eeece0d69834b875fe8b9fb30f and 089c0153470f89ece94a0bff13fed24147887e48, but I need to validate. 
  - [ ] Make sure the app doesn't superexplode when the content process just dies. 
    - [ ] Kill the content proc, mouse over the pane. TermControl explodes in PointerOver, cause the settings are gone. Deal with that.
      - Should we just set Closing when the content dies?
      - Should we add another check?
  - [ ] Make sure the content doesn't superexplode when the window process just dies. 
  - [ ] Make sure the app doesn't leak a content process when the content process exits. 
  - [ ] Make sure the app doesn't leak a content process when the tab closes (from UI). 
  - [ ] hot-reloading the settings crashes the window proc. I'm guessing the call to `_core.UpdateSettings` on the UI thread when OOP is what gets us. 
\<notes to self>

### 📝 `dev/migrie/oop/2/wandavision`

`move-pane -w <id>` to move a pane/tab to another Terminal window. This will prove we can move panes without the whole instantiation of a new window process.
**original diff at**:  in [`dev/migrie/oop/wandavision`](https://github.com/microsoft/terminal/compare/81a5a736fef41bd90339c36a6cc4d7a339b6badf...dev/migrie/oop/wandavision) (link to relevant diff from `dev/migrie/oop/endgame`)
**diff from `endgame`**: https://github.com/microsoft/terminal/compare/dev/migrie/oop/2/endgame...dev/migrie/oop/2/wandavision

#### Original prototype: 
* `TerminalPage::_MovePane` raises an event up, with the content guid of the current pane and a string describing the target window
* That's bubbled through `WindowManager` to `Monarch`, to ask the monarch to deal with this.
* Monarch tells the target peasant to `AttachPaneToWindow`, with the target tab index and content guid
* Peasant then raises an event to AppHost to get it to tell the TerminalPage to `TerminalPage::AttachPane`.

#### TODO

- [x] `RequestMoveContent(String window, String contentJson, Int tabIndex)`.
  - [x] `BuildStartupActions` in `Pane` and in `TabBase` accept a `const bool asContent = false` parameter, to serialize with a content guid
    - [x] `Pane`
    - [x] `TerminalTab`
    - [x] `SettingsTab`
  - [x] We'll need a (TerminalPage) local helper for serializing a `std::vector<ActionAndArgs>` to a json blob. 
    - We have one for a `IVector`, presumably? Looks like in ApplicationState.h, we just use `SetValueForKey(json, TabLayoutKey, val.TabLayout());`
    - We'll probably want to get rid of all whitespace in this serialization. 
  - [x] convert all the handlers we have around remoting, etc.
- [x] `newTab` needs to be smart enough to look for a `ContentGuid()` in the `NewTerminalArgs`. If it finds one, do the content process attach instead.
- [x] same with `splitPane`.
- 🦶 Move a pane to another window - **PUNTED** 🦶 
  - This simply isn't a priority. We can file a follow-up for `movePane(window)`. The tab-based code is MUCH easier to wrap your head around, and doesn't have the pane-wise edge cases.
  - [x] `MovePane` to another window raises a `RequestMoveContent` with the serialized form of the active pane.
  - [x] When TerminalPage handles MoveContentRequest, deserialize it...
  - [🦶] ... Look at the first action. If it's `splitPane`...
    - [🦶] if tabIndex is > tabs.size(), make a new tab.
      - "Move a pane to another window, as a tab" (`movePane(index=99999, window={window})`)
      - In #10780, "If the tab index is greater than the number of current tabs a new tab will be created with the pane as its root".
    - [x] else make a new pane.
      - "Move a pane to another window, as a pane"
- [x] Move a tab to another window
  - [x] `MovePane` to another window raises a `RequestMoveContent` with the serialized form of the active tab.
  - [x] ... Look at the first action. If it's `newTab`...
    - [x] just run the actions. `MoveContentArgs.tabIndex` is unused in this case
- [x] Wire up a callback s.t. the original window can close the tab/pane.
- [ ] If we try to `moveTab` to a window that doesn't exist, create a new window for that name, with that content? Is that possible?
  - Let's sort this out in parallel with the callback above. 
  - Like, we need to hang on to the content until someone else attaches oh my god it's so easy
  - [x] `contentProcess.Attached()` event. When the terminal `move(Tab|Pane)`s, add a callback to `Attached`. If we get that, we know someone else got it, and we can detach.
  - [x] Oh, uhg. But for a Tab, there's lots of contents (even for a pane). poop.
- [x] Fix the pane serialization. Dragging a single tab now creates a tab, AND a pane for that single control. Yikes. 

After this is all done, we definitely could plop the string into a [DataPackage](https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.datatransfer.datapackage) and unwrap that to drop tabs. 

As of 35c82d37f: (_commented out, there are better gifs below_)
<!-- ![wandavision-whoop-whoop](https://user-images.githubusercontent.com/18356694/179091488-73c94e89-fe62-4113-bfe3-ab457b2965b1.gif) -->


<notes to self>


### 📝 `dev/migrie/oop/2/loki`

* [x] add a `TabDragStarting` to the TabView. In there, stash the serialized tab under the "content" key
* [x] `TabStripDragOver` handler on TabView - look for the "content" key, and if it's there, accept it as a move
* [x] `TabView.TabStripDrop`: get the content, deserialize, and fire it off to the action handler
* [x] Make sure we can call back to the original window to remove the tab.

<!-- ![loki-one-shot-one-opportunity](https://user-images.githubusercontent.com/18356694/179215130-11dfa4ad-5256-41f5-b066-46e9ab954596.gif) -->

![loki-with-detach-too](https://user-images.githubusercontent.com/18356694/179283633-c9ff1a99-4774-4890-9932-00197042895c.gif)


I don't know if my issue related to the process model improvements, but I guess so. If I post it in a wrong thread, please move where needed.

Why don't you implement a **per-tab** execution policy for Windows Terminal? It would be very nice to have separate execution policies for different tabs, for everydays tasks Undefined policy is quite sufficient, but for some special development task like [creating Python virtual environment](https://github.com/PowerShell/PowerShell/issues/17080) I may need Unrestricted policy. It may be implemented by hotkey, or holding Ctrl-Shift on tab creation, whatever.

In the #632 you discussed why it is bad to have a mix of (non)elevated tabs, but here is a different thing. Changing execution policy and acquiring elevated permissions is not the same thing, and one can be done without another.
@Suncatcher Thanks for the request. It's a good idea! The catch here, though, is that the execution policy is a property of the thing that is running _inside_ terminal, right? I think it's actually specifically a PowerShell concept... and I don't believe there's a way using the Windows API to influence the execution policy of any instances of PowerShell under a process tree using `CreateProcess` (or its friends.) Even if there were, it wouldn't mean anything to `cmd.exe` or Cygwin or nushell or yori, simply because they don't have an equivalent concept. Perl does, somewhat, have a restricted mode. I don't know if that's equivalent... and perl doesn't see much interactive use.

The easiest way to accomplish this today is going to be to add new profiles that use PowerShell native arguments to set the execution policy you want, and open those profiles from the New Tab dropdown menu.

As an example...

<img width="406" alt="image" src="https://user-images.githubusercontent.com/189190/183763521-e8541136-6b21-4988-8793-bfc96bf64ab6.png">

<img width="1051" alt="image" src="https://user-images.githubusercontent.com/189190/183763582-60aeeae9-b3e6-4d4f-9b59-810ed037f7a5.png">

```json
            {
                "commandline": "\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\" -NoExit -Command \"Set-ExecutionPolicy Restricted -Scope Process\"",
                "guid": "{d4207635-ac89-4c36-803f-7566ab0e243b}",
                "hidden": false,
                "icon": "🔐",
                "name": "PowerShell (Restricted Policy)",
                "startingDirectory": "%USERPROFILE%",
                "background": "#330000"
            },
```
thanks, didn't know this capability to set PS arguments per profile. Will use it.

Fix patch:
diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp
index a62493ecb4a..3466e84d009 100644
--- a/src/cascadia/Remoting/Monarch.cpp
+++ b/src/cascadia/Remoting/Monarch.cpp
@@ -1058,7 +1058,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
 
     void Monarch::RequestMoveContent(winrt::hstring window,
                                      winrt::hstring content,
-                                     uint32_t tabIndex)
+                                     uint32_t tabIndex,
+                                     const Windows::Foundation::IReference<Windows::Foundation::Rect>& windowBounds)
     {
         TraceLoggingWrite(g_hRemotingProvider,
                           "Monarch_MoveContent_Requested",
@@ -1102,11 +1103,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
                               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)
+            // for that name / ID.
+            //
+            // Don't let the window literally be named "-1", because that's silly
+            auto request = winrt::make_self<implementation::WindowRequestedArgs>(window == L"-1" ? L"" : window,
+                                                                                 content,
+                                                                                 windowBounds);
+            _RequestNewWindowHandlers(*this, *request);
         }
     }
 
@@ -1132,16 +1136,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         }
         else
         {
+            // We couldn't find the peasant that started the drag. Well that
+            // sure is weird, but that would indicate that the sender closed
+            // after starting the drag. No matter. We can just do nothing.
+
             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 54c31e98e27..ef311fad4e1 100644
--- a/src/cascadia/Remoting/Monarch.h
+++ b/src/cascadia/Remoting/Monarch.h
@@ -48,12 +48,22 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
             _args{ command.Commandline() },
             _CurrentDirectory{ command.CurrentDirectory() } {};
 
+        WindowRequestedArgs(const winrt::hstring& window, const winrt::hstring& content, const Windows::Foundation::IReference<Windows::Foundation::Rect>& bounds) :
+            _Id{ 0u },
+            _WindowName{ window },
+            _args{},
+            _CurrentDirectory{},
+            _Content{ content },
+            _InitialBounds{ bounds } {};
+
         void Commandline(const winrt::array_view<const winrt::hstring>& value) { _args = { value.begin(), value.end() }; };
         winrt::com_array<winrt::hstring> Commandline() { return winrt::com_array<winrt::hstring>{ _args.begin(), _args.end() }; }
 
         WINRT_PROPERTY(uint64_t, Id);
         WINRT_PROPERTY(winrt::hstring, WindowName);
         WINRT_PROPERTY(winrt::hstring, CurrentDirectory);
+        WINRT_PROPERTY(winrt::hstring, Content);
+        WINRT_PROPERTY(Windows::Foundation::IReference<Windows::Foundation::Rect>, InitialBounds);
 
     private:
         winrt::com_array<winrt::hstring> _args;
@@ -81,7 +91,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         Windows::Foundation::Collections::IVectorView<winrt::Microsoft::Terminal::Remoting::PeasantInfo> GetPeasantInfos();
         Windows::Foundation::Collections::IVector<winrt::hstring> GetAllWindowLayouts();
 
-        void RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex);
+        void RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex, const Windows::Foundation::IReference<Windows::Foundation::Rect>& windowBounds);
         void RequestSendContent(const Remoting::RequestReceiveContentArgs& args);
 
         TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl
index 44ebf7b0f1c..bc60fbe2c71 100644
--- a/src/cascadia/Remoting/Monarch.idl
+++ b/src/cascadia/Remoting/Monarch.idl
@@ -26,6 +26,9 @@ namespace Microsoft.Terminal.Remoting
 
         String[] Commandline { get; };
         String CurrentDirectory { get; };
+
+        String Content { get; };
+        Windows.Foundation.IReference<Windows.Foundation.Rect> InitialBounds { get; };
     }
 
     [default_interface] runtimeclass SummonWindowSelectionArgs {
@@ -69,7 +72,7 @@ namespace Microsoft.Terminal.Remoting
         Windows.Foundation.Collections.IVectorView<PeasantInfo> GetPeasantInfos { get; };
         Windows.Foundation.Collections.IVector<String> GetAllWindowLayouts();
 
-        void RequestMoveContent(String window, String content, UInt32 tabIndex);
+        void RequestMoveContent(String window, String content, UInt32 tabIndex, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds);
         void RequestSendContent(RequestReceiveContentArgs args);
 
         event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp
index afa8403790e..6331e3e9eb8 100644
--- a/src/cascadia/Remoting/WindowManager.cpp
+++ b/src/cascadia/Remoting/WindowManager.cpp
@@ -416,10 +416,11 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
 
     winrt::fire_and_forget WindowManager::RequestMoveContent(winrt::hstring window,
                                                              winrt::hstring content,
-                                                             uint32_t tabIndex)
+                                                             uint32_t tabIndex,
+                                                             Windows::Foundation::IReference<Windows::Foundation::Rect> windowBounds)
     {
         co_await winrt::resume_background();
-        _monarch.RequestMoveContent(window, content, tabIndex);
+        _monarch.RequestMoveContent(window, content, tabIndex, windowBounds);
     }
 
     winrt::fire_and_forget WindowManager::RequestSendContent(Remoting::RequestReceiveContentArgs args)
diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h
index e42d6b4ba27..015ab9f3472 100644
--- a/src/cascadia/Remoting/WindowManager.h
+++ b/src/cascadia/Remoting/WindowManager.h
@@ -44,7 +44,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         Windows::Foundation::Collections::IVector<winrt::hstring> GetAllWindowLayouts();
         bool DoesQuakeWindowExist();
 
-        winrt::fire_and_forget RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex);
+        winrt::fire_and_forget RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex, Windows::Foundation::IReference<Windows::Foundation::Rect> windowBounds);
         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 ca9d78a5d1a..5dd72b7f19a 100644
--- a/src/cascadia/Remoting/WindowManager.idl
+++ b/src/cascadia/Remoting/WindowManager.idl
@@ -26,7 +26,7 @@ namespace Microsoft.Terminal.Remoting
 
         Boolean DoesQuakeWindowExist();
 
-        void RequestMoveContent(String window, String content, UInt32 tabIndex);
+        void RequestMoveContent(String window, String content, UInt32 tabIndex, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds);
         void RequestSendContent(RequestReceiveContentArgs args);
 
         event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp
index 560a531650b..457cf34c1ca 100644
--- a/src/cascadia/TerminalApp/TabManagement.cpp
+++ b/src/cascadia/TerminalApp/TabManagement.cpp
@@ -512,9 +512,9 @@ namespace winrt::TerminalApp::implementation
             _mruTabs.RemoveAt(mruIndex);
         }
 
-        if (_stashedDraggedTab && *_stashedDraggedTab == tab)
+        if (_stashed.draggedTab && *_stashed.draggedTab == tab)
         {
-            _stashedDraggedTab = nullptr;
+            _stashed.draggedTab = nullptr;
         }
 
         _tabs.RemoveAt(tabIndex);
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index efe73161ec0..9b0abe8904a 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -250,6 +250,7 @@ namespace winrt::TerminalApp::implementation
         _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting });
         _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver });
         _tabView.TabStripDrop({ this, &TerminalPage::_onTabStripDrop });
+        _tabView.TabDroppedOutside({ this, &TerminalPage::_onTabDroppedOutside });
 
         _CreateNewTabFlyout();
 
@@ -1985,13 +1986,18 @@ namespace winrt::TerminalApp::implementation
     //   and should be expected to be empty after this call.
     void TerminalPage::_MoveContent(std::vector<Settings::Model::ActionAndArgs>&& actions,
                                     const winrt::hstring& windowName,
-                                    const uint32_t tabIndex)
+                                    const uint32_t tabIndex,
+                                    const std::optional<til::point>& dragPoint)
     {
         const auto winRtActions{ winrt::single_threaded_vector<ActionAndArgs>(std::move(actions)) };
         const auto str{ ActionAndArgs::Serialize(winRtActions) };
         const auto request = winrt::make_self<RequestMoveContentArgs>(windowName,
                                                                       str,
                                                                       tabIndex);
+        if (dragPoint.has_value())
+        {
+            request->WindowPosition(dragPoint->to_winrt_point());
+        }
         _RequestMoveContentHandlers(*this, *request);
     }
 
@@ -2026,6 +2032,11 @@ namespace winrt::TerminalApp::implementation
         return true;
     }
 
+    uint32_t TerminalPage::NumberOfTabs() const
+    {
+        return _tabs.Size();
+    }
+
     // Method Description:
     // - Called when it is determined that an existing tab or pane should be
     //   attached to our window. content represents a blob of JSON describing
@@ -2036,11 +2047,9 @@ namespace winrt::TerminalApp::implementation
     //   reattach instead of create new content, so this method simply needs to
     //   parse the JSON and pump it into our action handler. Almost the same as
     //   doing something like `wt -w 0 nt`.
-    winrt::fire_and_forget TerminalPage::AttachContent(winrt::hstring content,
+    winrt::fire_and_forget TerminalPage::AttachContent(IVector<Settings::Model::ActionAndArgs> args,
                                                        uint32_t tabIndex)
     {
-        auto args = ActionAndArgs::Deserialize(content);
-
         if (args == nullptr ||
             args.Size() == 0)
         {
@@ -2064,18 +2073,6 @@ namespace winrt::TerminalApp::implementation
         {
             _SelectTab(tabIndex);
         }
-        else
-        {
-            if (firstIsSplitPane)
-            {
-                // Create the equivalent NewTab action.
-                const auto newAction = Settings::Model::ActionAndArgs{ Settings::Model::ShortcutAction::NewTab,
-                                                                       Settings::Model::NewTabArgs(firstAction.Args() ?
-                                                                                                       firstAction.Args().try_as<Settings::Model::SplitPaneArgs>().TerminalArgs() :
-                                                                                                       nullptr) };
-                args.SetAt(0, newAction);
-            }
-        }
 
         for (const auto& action : args)
         {
@@ -4611,7 +4608,23 @@ namespace winrt::TerminalApp::implementation
         {
             // First: stash the tab we started dragging.
             // We're going to be asked for this.
-            _stashedDraggedTab = tabImpl;
+            _stashed.draggedTab = tabImpl;
+
+            // Stash the offset from where we started the drag to the
+            // tab's origin. We'll use that offset in the future to help
+            // position the dropped window.
+
+            // First, the position of the pointer, from the CoreWindow
+            const til::point pointerPosition{ til::math::rounding, CoreWindow::GetForCurrentThread().PointerPosition() };
+            // Next, the position of the tab itself:
+            const til::point tabPosition{ til::math::rounding, eventTab.TransformToVisual(nullptr).TransformPoint({ 0, 0 }) };
+            // Now, we need to add the origin of our CoreWindow to the tab
+            // position.
+            const auto& coreWindowBounds{ CoreWindow::GetForCurrentThread().Bounds() };
+            const til::point windowOrigin{ til::math::rounding, coreWindowBounds.X, coreWindowBounds.Y };
+            const auto realTabPosition = windowOrigin + tabPosition;
+            // Subtract the two to get the offset.
+            _stashed.dragOffset = til::point{ pointerPosition - realTabPosition };
 
             // Into the DataPackage, let's stash our own window ID.
             const auto id{ _WindowProperties.WindowId() };
@@ -4630,7 +4643,8 @@ namespace winrt::TerminalApp::implementation
             //      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
+            //    * This will be handled by _onTabDroppedOutside, which will
+            //      raise a MoveContent (to a new window) event.
         }
     }
 
@@ -4736,7 +4750,36 @@ namespace winrt::TerminalApp::implementation
         {
             co_return;
         }
-        if (!_stashedDraggedTab)
+        if (!_stashed.draggedTab)
+        {
+            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.
+
+            _sendDraggedTabToWindow(winrt::hstring{ fmt::format(L"{}", args.TargetWindow()) },
+                                    args.TabIndex(),
+                                    std::nullopt);
+        }
+    }
+
+    winrt::fire_and_forget TerminalPage::_onTabDroppedOutside(winrt::IInspectable sender,
+                                                              winrt::MUX::Controls::TabViewTabDroppedOutsideEventArgs e)
+    {
+        // Get the current pointer point from the CoreWindow
+        const auto& pointerPoint{ CoreWindow::GetForCurrentThread().PointerPosition() };
+
+        // This is called when a tab FROM OUR WINDOW was dropped outside the
+        // tabview. We already know which tab was being dragged. We'll just
+        // invoke a moveTab action with the target window being -1. That will
+        // force the creation of a new window.
+
+        if (!_stashed.draggedTab)
         {
             co_return;
         }
@@ -4747,12 +4790,29 @@ namespace winrt::TerminalApp::implementation
         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);
+
+            // We need to convert the pointer point to a point that we can use
+            // to position the new window. We'll use the drag offset from before
+            // so that the tab in the new window is positioned so that it's
+            // basically still directly under the cursor.
+
+            // -1 is the magic number for "new window"
+            // 0 as the tab index, because we don't care. It's making a new window. It'll be the only tab.
+            const til::point adjusted = til::point{ til::math::rounding, pointerPoint } - _stashed.dragOffset;
+            _sendDraggedTabToWindow(winrt::hstring{ L"-1" }, 0, adjusted);
         }
     }
 
+    void TerminalPage::_sendDraggedTabToWindow(const winrt::hstring& windowId,
+                                               const uint32_t tabIndex,
+                                               std::optional<til::point> dragPoint)
+    {
+        auto startupActions = _stashed.draggedTab->BuildStartupActions(true);
+        _DetachTabFromWindow(_stashed.draggedTab);
+
+        _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint);
+        // _RemoveTab will make sure to null out the _stashed.draggedTab
+        _RemoveTab(*_stashed.draggedTab);
+    }
+
 }
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 46cf9c11c8c..efc68f5f6b2 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -67,6 +67,7 @@ namespace winrt::TerminalApp::implementation
         WINRT_PROPERTY(winrt::hstring, Window);
         WINRT_PROPERTY(winrt::hstring, Content);
         WINRT_PROPERTY(uint32_t, TabIndex);
+        WINRT_PROPERTY(Windows::Foundation::IReference<Windows::Foundation::Point>, WindowPosition);
 
     public:
         RequestMoveContentArgs(const winrt::hstring window, const winrt::hstring content, uint32_t tabIndex) :
@@ -162,9 +163,11 @@ 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 AttachContent(Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::ActionAndArgs> args, uint32_t tabIndex);
         winrt::fire_and_forget SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args);
 
+        uint32_t NumberOfTabs() const;
+
         WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
 
         // -------------------------------- WinRT Events ---------------------------------
@@ -263,7 +266,11 @@ namespace winrt::TerminalApp::implementation
 
         TerminalApp::ContentManager _manager{ nullptr };
 
-        winrt::com_ptr<winrt::TerminalApp::implementation::TabBase> _stashedDraggedTab{ nullptr };
+        struct StashedDragData
+        {
+            winrt::com_ptr<winrt::TerminalApp::implementation::TabBase> draggedTab{ nullptr };
+            til::point dragOffset{ 0, 0 };
+        } _stashed;
 
         winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::NewConnection_revoker _newConnectionRevoker;
 
@@ -503,10 +510,15 @@ namespace winrt::TerminalApp::implementation
         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);
+        winrt::fire_and_forget _onTabDroppedOutside(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::UI::Xaml::Controls::TabViewTabDroppedOutsideEventArgs e);
 
         void _DetachPaneFromWindow(std::shared_ptr<Pane> pane);
         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 _MoveContent(std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>&& actions,
+                          const winrt::hstring& windowName,
+                          const uint32_t tabIndex,
+                          const std::optional<til::point>& dragPoint = std::nullopt);
+        void _sendDraggedTabToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional<til::point> dragPoint);
 
         void _ContextMenuOpened(const IInspectable& sender, const IInspectable& args);
         void _SelectionMenuOpened(const IInspectable& sender, const IInspectable& args);
diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl
index a23203046dd..5c8a151b909 100644
--- a/src/cascadia/TerminalApp/TerminalPage.idl
+++ b/src/cascadia/TerminalApp/TerminalPage.idl
@@ -29,6 +29,7 @@ namespace TerminalApp
         String Window { get; };
         String Content { get; };
         UInt32 TabIndex { get; };
+        Windows.Foundation.IReference<Windows.Foundation.Point> WindowPosition { get; };
     };
     [default_interface] runtimeclass RequestReceiveContentArgs {
         RequestReceiveContentArgs(UInt64 src, UInt64 tgt, UInt32 tabIndex);
@@ -77,7 +78,6 @@ namespace TerminalApp
         String KeyboardServiceDisabledText { get; };
 
         TaskbarState TaskbarState{ get; };
-        void AttachContent(String content, UInt32 tabIndex);
 
         Windows.UI.Xaml.Media.Brush TitlebarBrush { get; };
         void WindowActivated(Boolean activated);
diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp
index 3db175daec8..dd984c6c126 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.cpp
+++ b/src/cascadia/TerminalApp/TerminalWindow.cpp
@@ -23,6 +23,7 @@ using namespace winrt::Windows::System;
 using namespace winrt::Microsoft::Terminal;
 using namespace winrt::Microsoft::Terminal::Control;
 using namespace winrt::Microsoft::Terminal::Settings::Model;
+using namespace winrt::Windows::Foundation::Collections;
 using namespace ::TerminalApp;
 
 namespace winrt
@@ -144,23 +145,33 @@ namespace winrt::TerminalApp::implementation
         _root = winrt::make_self<TerminalPage>(*_WindowProperties, _manager);
         _dialog = ContentDialog{};
 
-        // Pass commandline args into the TerminalPage. If we were supposed to
-        // load from a persisted layout, do that instead.
-
-        // layout will only ever be non-null if there were >0 tabs persisted in
-        // .TabLayout(). We can re-evaluate that as a part of TODO: GH#12633
-        if (const auto& layout = LoadPersistedLayout())
+        // Pass in information about the initial state of the window.
+        // * If we were supposed to start from serialized "content", do that,
+        // * If we were supposed to load from a persisted layout, do that
+        //   instead.
+        // * if we have commandline arguments, Pass commandline args into the
+        //   TerminalPage.
+        if (!_initialContentArgs.empty())
         {
-            std::vector<Settings::Model::ActionAndArgs> actions;
-            for (const auto& a : layout.TabLayout())
-            {
-                actions.emplace_back(a);
-            }
-            _root->SetStartupActions(actions);
+            _root->SetStartupActions(_initialContentArgs);
         }
         else
         {
-            _root->SetStartupActions(_appArgs.GetStartupActions());
+            // layout will only ever be non-null if there were >0 tabs persisted in
+            // .TabLayout(). We can re-evaluate that as a part of TODO: GH#12633
+            if (const auto& layout = LoadPersistedLayout())
+            {
+                std::vector<Settings::Model::ActionAndArgs> actions;
+                for (const auto& a : layout.TabLayout())
+                {
+                    actions.emplace_back(a);
+                }
+                _root->SetStartupActions(actions);
+            }
+            else
+            {
+                _root->SetStartupActions(_appArgs.GetStartupActions());
+            }
         }
 
         // Check if we were started as a COM server for inbound connections of console sessions
@@ -176,6 +187,7 @@ namespace winrt::TerminalApp::implementation
 
         return _root->Initialize(hwnd);
     }
+
     // Method Description:
     // - Called around the codebase to discover if this is a UWP where we need to turn off specific settings.
     // Arguments:
@@ -598,6 +610,17 @@ namespace winrt::TerminalApp::implementation
                                                               commandlineSize.height);
         }
 
+        if (_contentBounds)
+        {
+            // If we've been created as a torn-out window, then we'll need to
+            // use that size instead. _contentBounds is in raw pixels. Huzzah!
+            // Just return that.
+            return {
+                _contentBounds.Value().Width,
+                _contentBounds.Value().Height
+            };
+        }
+
         // GH#2061 - If the global setting "Always show tab bar" is
         // set or if "Show tabs in title bar" is set, then we'll need to add
         // the height of the tab bar here.
@@ -645,6 +668,11 @@ namespace winrt::TerminalApp::implementation
     // - LaunchMode enum that indicates the launch mode
     LaunchMode TerminalWindow::GetLaunchMode()
     {
+        if (_contentBounds)
+        {
+            return LaunchMode::DefaultMode;
+        }
+
         // GH#4620/#5801 - If the user passed --maximized or --fullscreen on the
         // commandline, then use that to override the value from the settings.
         const auto valueFromSettings = _settings.GlobalSettings().LaunchMode();
@@ -683,12 +711,24 @@ namespace winrt::TerminalApp::implementation
             }
         }
 
-        // Commandline args trump everything else
+        // Commandline args trump everything except for content bounds (tear-out)
         if (_appArgs.GetPosition().has_value())
         {
             initialPosition = _appArgs.GetPosition().value();
         }
 
+        if (_contentBounds)
+        {
+            // If the user has specified a contentBounds, then we should use
+            // that to determine the initial position of the window. This is
+            // used when the user is dragging a tab out of the window, to create
+            // a new window.
+            //
+            // contentBounds is in screen pixels, but that's okay! we want to
+            // return screen pixels out of here. Nailed it.
+            const til::rect bounds = { til::math::rounding, _contentBounds.Value() };
+            initialPosition = { bounds.left, bounds.top };
+        }
         return {
             initialPosition.X ? initialPosition.X.Value() : defaultInitialX,
             initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY
@@ -697,8 +737,11 @@ namespace winrt::TerminalApp::implementation
 
     bool TerminalWindow::CenterOnLaunch()
     {
-        // If the position has been specified on the commandline, don't center on launch
-        return _settings.GlobalSettings().CenterOnLaunch() && !_appArgs.GetPosition().has_value();
+        // If
+        // * the position has been specified on the commandline,
+        // * We're opening the window as a part of tear out (and _contentBounds were set)
+        // then don't center on launch
+        return !_contentBounds && _settings.GlobalSettings().CenterOnLaunch() && !_appArgs.GetPosition().has_value();
     }
 
     // Method Description:
@@ -1008,6 +1051,19 @@ namespace winrt::TerminalApp::implementation
         return result;
     }
 
+    void TerminalWindow::SetStartupContent(const winrt::hstring& content,
+                                           const Windows::Foundation::IReference<Windows::Foundation::Rect>& bounds)
+    {
+        _contentBounds = bounds;
+
+        const auto& args = _contentStringToActions(content, true);
+
+        for (const auto& action : args)
+        {
+            _initialContentArgs.push_back(action);
+        }
+    }
+
     // Method Description:
     // - Parse the provided commandline arguments into actions, and try to
     //   perform them immediately.
@@ -1175,11 +1231,63 @@ namespace winrt::TerminalApp::implementation
         _WindowProperties->WindowId(id);
     }
 
+    // Method Description:
+    // - Deserialize this string of content into a list of actions to perform.
+    //   If replaceFirstWithNewTab is true and the first serialized action is a
+    //   `splitPane` action, we'll attempt to replace that action with the
+    //   equivalent `newTab` action.
+    IVector<Settings::Model::ActionAndArgs> TerminalWindow::_contentStringToActions(const winrt::hstring& content,
+                                                                                    const bool replaceFirstWithNewTab)
+    {
+        try
+        {
+            const auto& args = ActionAndArgs::Deserialize(content);
+            if (args == nullptr ||
+                args.Size() == 0)
+            {
+                return args;
+            }
+
+            const auto& firstAction = args.GetAt(0);
+            const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane };
+            if (replaceFirstWithNewTab &&
+                firstIsSplitPane)
+            {
+                // Create the equivalent NewTab action.
+                const auto newAction = Settings::Model::ActionAndArgs{ Settings::Model::ShortcutAction::NewTab,
+                                                                       Settings::Model::NewTabArgs(firstAction.Args() ?
+                                                                                                       firstAction.Args().try_as<Settings::Model::SplitPaneArgs>().TerminalArgs() :
+                                                                                                       nullptr) };
+                args.SetAt(0, newAction);
+            }
+
+            return args;
+        }
+        catch (...)
+        {
+            LOG_CAUGHT_EXCEPTION();
+        }
+
+        return nullptr;
+    }
+
     void TerminalWindow::AttachContent(winrt::hstring content, uint32_t tabIndex)
     {
         if (_root)
         {
-            _root->AttachContent(content, tabIndex);
+            // `splitPane` allows the user to specify which tab to split. In that
+            // case, split specifically the requested pane.
+            //
+            // If there's not enough tabs, then just turn this pane into a new tab.
+            //
+            // If the first action is `newTab`, the index is always going to be 0,
+            // so don't do anything in that case.
+
+            const bool replaceFirstWithNewTab = tabIndex >= _root->NumberOfTabs();
+
+            const auto& args = _contentStringToActions(content, replaceFirstWithNewTab);
+
+            _root->AttachContent(args, tabIndex);
         }
     }
     void TerminalWindow::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args)
diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h
index eda5c9e84ab..adb2c9b6638 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.h
+++ b/src/cascadia/TerminalApp/TerminalWindow.h
@@ -74,6 +74,7 @@ namespace winrt::TerminalApp::implementation
         bool HasCommandlineArguments() const noexcept;
 
         int32_t SetStartupCommandline(array_view<const winrt::hstring> actions);
+        void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference<Windows::Foundation::Rect>& contentBounds);
         int32_t ExecuteCommandline(array_view<const winrt::hstring> actions, const winrt::hstring& cwd);
         void SetSettingsStartupArgs(const std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions);
         winrt::hstring ParseCommandlineMessage();
@@ -171,6 +172,7 @@ namespace winrt::TerminalApp::implementation
         ::TerminalApp::AppCommandlineArgs _appArgs;
         bool _gotSettingsStartupActions{ false };
         std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs> _settingsStartupArgs{};
+        Windows::Foundation::IReference<Windows::Foundation::Rect> _contentBounds{ nullptr };
 
         winrt::com_ptr<TerminalApp::implementation::WindowProperties> _WindowProperties{ nullptr };
 
@@ -181,6 +183,7 @@ namespace winrt::TerminalApp::implementation
         TerminalApp::SettingsLoadEventArgs _initialLoadResult{ nullptr };
 
         TerminalApp::ContentManager _manager{ nullptr };
+        std::vector<Microsoft::Terminal::Settings::Model::ActionAndArgs> _initialContentArgs;
 
         void _ShowLoadErrorsDialog(const winrt::hstring& titleKey,
                                    const winrt::hstring& contentKey,
@@ -193,6 +196,10 @@ namespace winrt::TerminalApp::implementation
         void _RefreshThemeRoutine();
         void _OnLoaded(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs);
         void _OpenSettingsUI();
+
+        winrt::Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::ActionAndArgs> _contentStringToActions(const winrt::hstring& content,
+                                                                                                                                      const bool replaceFirstWithNewTab);
+
         // These are events that are handled by the TerminalPage, but are
         // exposed through the AppLogic. This macro is used to forward the event
         // directly to them.
diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl
index 770289895e2..825afa44c4a 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.idl
+++ b/src/cascadia/TerminalApp/TerminalWindow.idl
@@ -54,7 +54,7 @@ namespace TerminalApp
         Boolean HasCommandlineArguments();
 
         Int32 SetStartupCommandline(String[] commands);
-
+        void SetStartupContent(String json, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds);
         Int32 ExecuteCommandline(String[] commands, String cwd);
         String ParseCommandlineMessage { get; };
         Boolean ShouldExitEarly { get; };
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index fcca688c2c4..028971bd210 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -1258,12 +1258,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             return true;
         }
 
-        // TODO: GH#5000
-        // The Core owning the keybindings is weird. That's for sure. In the
-        // future, we may want to pass the keybindings into the control
-        // separately, so the control can have a pointer to an in-proc
-        // Keybindings object, rather than routing through the ControlCore.
-        // (see GH#5000)
         auto bindings = _core.Settings().KeyBindings();
         if (!bindings)
         {
@@ -2877,18 +2871,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
     IControlSettings TermControl::Settings() const
     {
-        // TODO: GH#5000
-        // We still need this in a couple places:
-        // - Pane.cpp uses this for parsing out the StartingTitle, Commandline,
-        //   etc for Pane::GetTerminalArgsForPane.
-        // - TerminalTab::_CreateToolTipTitle uses the ProfileName for the
-        //   tooltip for the tab.
-        //
-        // These both happen on the UI thread right now. In the future, when we
-        // have to hop across the process boundary to get at the core settings,
-        // it may make sense to cache these values inside the TermControl
-        // itself, so it can do the hop once when it's first setup, rather than
-        // when it's needed by the UI thread.
         return _core.Settings();
     }
 
diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp
index 058a6c0793c..0599aa980af 100644
--- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp
+++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp
@@ -118,7 +118,7 @@ namespace RemotingUnitTests
         bool DoesQuakeWindowExist() DIE;
         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 RequestMoveContent(winrt::hstring, winrt::hstring, uint32_t, winrt::Windows::Foundation::IReference<winrt::Windows::Foundation::Rect>) DIE;
         void RequestSendContent(Remoting::RequestReceiveContentArgs) DIE;
 
         TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, Remoting::FindTargetWindowArgs);
diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp
index b4958ece432..3776017f25e 100644
--- a/src/cascadia/WindowsTerminal/AppHost.cpp
+++ b/src/cascadia/WindowsTerminal/AppHost.cpp
@@ -38,7 +38,7 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic,
     _windowLogic{ nullptr },
     _window{ nullptr }
 {
-    _HandleCommandlineArgs();
+    _HandleCommandlineArgs(args);
 
     // _HandleCommandlineArgs will create a _windowLogic
     _useNonClientArea = _windowLogic.GetShowTabsInTitlebar();
@@ -157,8 +157,6 @@ void AppHost::s_DisplayMessageBox(const winrt::TerminalApp::ParseCommandlineResu
 //   the WindowManager, to ask if we should become a window process.
 // - If we should create a window, then pass the arguments to the app logic for
 //   processing.
-// - If we shouldn't become a window, set _shouldCreateWindow to false and exit
-//   immediately.
 // - If the logic determined there's an error while processing that commandline,
 //   display a message box to the user with the text of the error, and exit.
 //    * We display a message box because we're a Win32 application (not a
@@ -169,7 +167,7 @@ void AppHost::s_DisplayMessageBox(const winrt::TerminalApp::ParseCommandlineResu
 // - <none>
 // Return Value:
 // - <none>
-void AppHost::_HandleCommandlineArgs()
+void AppHost::_HandleCommandlineArgs(const Remoting::WindowRequestedArgs& windowArgs)
 {
     // We did want to make a window, so let's instantiate it here.
     // We don't have XAML yet, but we do have other stuff.
@@ -178,7 +176,12 @@ void AppHost::_HandleCommandlineArgs()
     if (_peasant)
     {
         const auto& args{ _peasant.InitialArgs() };
-        if (args)
+
+        if (!windowArgs.Content().empty())
+        {
+            _windowLogic.SetStartupContent(windowArgs.Content(), windowArgs.InitialBounds());
+        }
+        else if (args)
         {
             const auto result = _windowLogic.SetStartupCommandline(args.Commandline());
             const auto message = _windowLogic.ParseCommandlineMessage();
@@ -202,7 +205,6 @@ void AppHost::_HandleCommandlineArgs()
         // then we're going to exit immediately.
         if (_windowLogic.ShouldImmediatelyHandoffToElevated())
         {
-            _shouldCreateWindow = false;
             _windowLogic.HandoffToElevated();
             return;
         }
@@ -796,11 +798,6 @@ void AppHost::_WindowMouseWheeled(const til::point coord, const int32_t delta)
     }
 }
 
-bool AppHost::HasWindow()
-{
-    return _shouldCreateWindow;
-}
-
 // Method Description:
 // - Event handler for the Peasant::ExecuteCommandlineRequested event. Take the
 //   provided commandline args, and attempt to parse them and perform the
@@ -1221,10 +1218,77 @@ winrt::TerminalApp::TerminalWindow AppHost::Logic()
     return _windowLogic;
 }
 
+// Method Description:
+// - Raised from Page -> us -> manager -> monarch
+// - Called when the user attempts to move a tab or pane to another window.
+//   `args` will contain info about the structure of the content being moved,
+//   and where it should go.
+// - If the WindowPosition is filled in, then the user was dragging a tab out of
+//   this window and dropping it in empty space, indicating it should create a
+//   new window. In that case, we'll make some adjustments using that info and
+//   our own window info, so that the new window will be created in the right
+//   place and with the same size.
 void AppHost::_handleMoveContent(const winrt::Windows::Foundation::IInspectable& /*sender*/,
                                  winrt::TerminalApp::RequestMoveContentArgs args)
 {
-    _windowManager.RequestMoveContent(args.Window(), args.Content(), args.TabIndex());
+    winrt::Windows::Foundation::Rect rect{};
+    winrt::Windows::Foundation::IReference<winrt::Windows::Foundation::Rect> windowBoundsReference{ nullptr };
+
+    if (args.WindowPosition() && _window)
+    {
+        // The WindowPosition is in DIPs. We need to convert it to pixels.
+        const til::point dragPositionInDips{ til::math::rounding, args.WindowPosition().Value() };
+        const auto scale = _window->GetCurrentDpiScale();
+
+        til::point dragPositionInPixels{
+            til::math::rounding,
+            dragPositionInDips.x * scale,
+            dragPositionInDips.y * scale,
+        };
+
+        // Fortunately, the window position is already in pixels.
+        til::rect windowBoundsInPixels{ _window->GetWindowRect() };
+        til::size windowSize{ windowBoundsInPixels.size() };
+
+        const auto dpi = _window->GetCurrentDpi();
+        const auto nonClientFrame = _window->GetNonClientFrame(dpi);
+
+        // If this window is maximized, you don't _really_ want the new window
+        // showing up at the same size (the size of a maximized window). You
+        // want it to just make a normal-sized window. This logic was taken out
+        // of AppHost::_HandleCreateWindow.
+        if (IsZoomed(_window->GetHandle()))
+        {
+            const auto initialSize = _windowLogic.GetLaunchDimensions(dpi);
+
+            const auto islandWidth = Utils::ClampToShortMax(static_cast<long>(ceil(initialSize.Width)), 1);
+            const auto islandHeight = Utils::ClampToShortMax(static_cast<long>(ceil(initialSize.Height)), 1);
+
+            // Get the size of a window we'd need to host that client rect. This will
+            // add the titlebar space.
+            const til::size nonClientSize{ _window->GetTotalNonClientExclusiveSize(dpi) };
+
+            const auto adjustedWidth = islandWidth + nonClientSize.width;
+            const auto adjustedHeight = islandHeight + nonClientSize.height;
+
+            windowSize = til::size{ Utils::ClampToShortMax(adjustedWidth, 1),
+                                    Utils::ClampToShortMax(adjustedHeight, 1) };
+        }
+
+        // Adjust for the non-client bounds
+        dragPositionInPixels.x -= nonClientFrame.left;
+        dragPositionInPixels.y -= nonClientFrame.top;
+        windowSize = windowSize - nonClientFrame.size();
+
+        // Use the drag event as the new position, and the size of the actual window.
+        rect = winrt::Windows::Foundation::Rect{ static_cast<float>(dragPositionInPixels.x),
+                                                 static_cast<float>(dragPositionInPixels.y),
+                                                 static_cast<float>(windowSize.width),
+                                                 static_cast<float>(windowSize.height) };
+        windowBoundsReference = rect;
+    }
+
+    _windowManager.RequestMoveContent(args.Window(), args.Content(), args.TabIndex(), windowBoundsReference);
 }
 
 void AppHost::_handleAttach(const winrt::Windows::Foundation::IInspectable& /*sender*/,
diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h
index 3521d984b6a..dc2c3da91cc 100644
--- a/src/cascadia/WindowsTerminal/AppHost.h
+++ b/src/cascadia/WindowsTerminal/AppHost.h
@@ -21,7 +21,6 @@ class AppHost
     bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down);
     void SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
 
-    bool HasWindow();
     winrt::TerminalApp::TerminalWindow Logic();
 
     static void s_DisplayMessageBox(const winrt::TerminalApp::ParseCommandlineResult& message);
@@ -39,14 +38,13 @@ class AppHost
 
     winrt::com_ptr<IVirtualDesktopManager> _desktopManager{ nullptr };
 
-    bool _shouldCreateWindow{ false };
     bool _useNonClientArea{ false };
 
     std::shared_ptr<ThrottledFuncTrailing<bool>> _showHideWindowThrottler;
 
     void _preInit();
 
-    void _HandleCommandlineArgs();
+    void _HandleCommandlineArgs(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args);
     winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition();
 
     void _HandleCreateWindow(const HWND hwnd, til::rect proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode);
diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
index 75fe386d6c6..3343004aa0b 100644
--- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp
+++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
@@ -114,20 +114,41 @@ void WindowEmperor::_createNewWindowThread(const Remoting::WindowRequestedArgs&
     auto window{ std::make_shared<WindowThread>(_app.Logic(), args, _manager, peasant) };
     std::weak_ptr<WindowEmperor> weakThis{ weak_from_this() };
 
-    std::thread t([weakThis, window]() {
-        window->CreateHost();
+    // Increment our count of window instances _now_, immediately. We're
+    // starting a window now, we shouldn't exit (due to having 0 windows) till
+    // this window has a chance to actually start.
+    // * We can't just put the window immediately into _windows right now,
+    //   because there are multiple async places where we iterate over all
+    //   _windows assuming that they have initialized and we can call methods
+    //   that might hit the TerminalPage.
+    // * If we don't somehow track this window now, before it has been actually
+    //   started, there's a possible race. As an example, it would be possible
+    //   to drag a tab out of the single window, which would create a new
+    //   window, but have the original window exit before the new window has
+    //   started, causing the app to exit.
+    // Hence: increment the number of total windows now.
+    _windowThreadInstances.fetch_add(1, std::memory_order_relaxed);
 
-        if (auto self{ weakThis.lock() })
+    std::thread t([weakThis, window]() {
+        try
         {
-            self->_windowStartedHandler(window);
-        }
+            const auto cleanup = wil::scope_exit([&]() {
+                if (auto self{ weakThis.lock() })
+                {
+                    self->_windowExitedHandler(window->Peasant().GetID());
+                }
+            });
 
-        window->RunMessagePump();
+            window->CreateHost();
 
-        if (auto self{ weakThis.lock() })
-        {
-            self->_windowExitedHandler(window->Peasant().GetID());
+            if (auto self{ weakThis.lock() })
+            {
+                self->_windowStartedHandlerPostXAML(window);
+            }
+
+            window->RunMessagePump();
         }
+        CATCH_LOG()
     });
     LOG_IF_FAILED(SetThreadDescription(t.native_handle(), L"Window Thread"));
 
@@ -140,7 +161,7 @@ void WindowEmperor::_createNewWindowThread(const Remoting::WindowRequestedArgs&
 // Q: Why isn't adding these callbacks just a part of _createNewWindowThread?
 // A: Until the thread actually starts, the AppHost (and its Logic()) haven't
 // been ctor'd or initialized, so trying to add callbacks immediately will A/V
-void WindowEmperor::_windowStartedHandler(const std::shared_ptr<WindowThread>& sender)
+void WindowEmperor::_windowStartedHandlerPostXAML(const std::shared_ptr<WindowThread>& sender)
 {
     // Add a callback to the window's logic to let us know when the window's
     // quake mode state changes. We'll use this to check if we need to add
@@ -181,7 +202,7 @@ void WindowEmperor::_windowExitedHandler(uint64_t senderID)
                       return w->Peasant().GetID() == senderID;
                   });
 
-    if (lockedWindows->size() == 0)
+    if (_windowThreadInstances.fetch_sub(1, std::memory_order_relaxed) == 1)
     {
         _close();
     }
diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h
index 9ed30a41cea..73e3f1ebbe1 100644
--- a/src/cascadia/WindowsTerminal/WindowEmperor.h
+++ b/src/cascadia/WindowsTerminal/WindowEmperor.h
@@ -41,6 +41,7 @@ class WindowEmperor : public std::enable_shared_from_this<WindowEmperor>
     winrt::Microsoft::Terminal::Remoting::WindowManager _manager;
 
     til::shared_mutex<std::vector<std::shared_ptr<WindowThread>>> _windows;
+    std::atomic<uint32_t> _windowThreadInstances;
 
     std::optional<til::throttled_func_trailing<>> _getWindowLayoutThrottler;
 
@@ -51,7 +52,7 @@ class WindowEmperor : public std::enable_shared_from_this<WindowEmperor>
 
     std::unique_ptr<NotificationIcon> _notificationIcon;
 
-    void _windowStartedHandler(const std::shared_ptr<WindowThread>& sender);
+    void _windowStartedHandlerPostXAML(const std::shared_ptr<WindowThread>& sender);
     void _windowExitedHandler(uint64_t senderID);
 
     void _becomeMonarch();


Test patch: