Repository: microsoft/terminal (language: cpp)

Issue:
Support broadcast input?
<!-- 
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨

I ACKNOWLEDGE THE FOLLOWING BEFORE PROCEEDING:
1. If I delete this entire template and go my own path, the core team may close my issue without further explanation or engagement.
2. If I list multiple bugs/concerns in this one issue, the core team may close my issue without further explanation or engagement.
3. If I write an issue that has many duplicates, the core team may close my issue without further explanation or engagement (and without necessarily spending time to find the exact duplicate ID number).
4. If I leave the title incomplete when filing the issue, the core team may close my issue without further explanation or engagement.
5. If I file something completely blank in the body, the core team may close my issue without further explanation or engagement.

All good? Then proceed!
-->

# Description of the new feature/enhancement

Sometimes i need to open many panes in a tab with splitHorizontal/splitVertical hotkeys. And then broadcast input to all the panes in current tab, or all tabs.

Is it possible to support broadcast input, like iterm2?

![image](https://user-images.githubusercontent.com/2578976/64075757-fa971980-ccee-11e9-9e44-47aaf3bca76c.png)


<!-- 
A clear and concise description of what the problem is that the new feature would solve.
Describe why and how a user would use this new functionality (if applicable).
-->


<!-- 
A clear and concise description of what you want to happen.
-->



Discussion / hints:
I suppose there's no reason we _couldn't_ support this. 

It might need some refactoring to have a `TermControl` raise an event up to the `App` layer to have it transmit the input to all the terminals in the tab/window, but that's certainly possible.
@zadjii-msft    
It is useful to maintain a series of servers with the same role. And now other popular terminals all support this feature, like iterm2 on OS X, cmder/conemu on windows, xshell/securCRT and so on. 

SO PLEASE consider adding this feature!
> 
> # Send the same command to all the active tabs within windows terminal
> 
> I hope windows terminal come with a feature that it can send the same command to all the active tabs.
> Let's say we open 4 tabs within terminal,then each tab have a connection to a SSH server. and we want to know the status of the firewall on those server by issue "systemctl status firewalld" . but we want do it in one time. not one by one for each connection in these 4 tabs.
> # This feature had been implement in SecureCRT.
> 
> Bellow is the steps to send the same command to all active tabs in SecureCRT.
> 
> This can be done using the 'Chat Window'.
> 
>     1. Select 'Chat Window' from the 'View' menu so that the window will be displayed.
> 
>     2. Then just right click inside the 'Chat Window' and select the 'Send Chat to All Tabs' option in the pop-up menu.
> 
>     3. Once enabled, any commands entered in the 'Chat Window' will be sent to all tabs when 'Enter' is pressed.
> 
> 
> # It will be very handy in daily task if it can be like that.

##### Originally posted by @tlsalex in #3352

We sure need that~
I am also very interested in this feature, and want to emphasize the multiple modes of broadcast operation that iTerm2 supports. It's important that broadcasting to all panes _in the current tab_ be supported distinctly from broadcasting to all panes in the window.
This is the only missing feature keeping this terminal from being used ubiquitously in my organization. Please add support for this!
Just throwing my six cents in on this, very interested as I've come from iTerm2  and also would find it very useful if there were options to broadcast to all panes in the current tab, as well as all panes in all tabs.
Terminator also has the concept of groups. You can assign a pane to a named group and this allows broadcast to the group as well as all panes. This is very useful in the situation where I have both DB server and application server panes but want to broadcast to all of a particular type.
See here for an example: https://terminator-gtk2.readthedocs.io/en/latest/grouping.html
Maybe it would also be very helpful if we can choose which panes to sync input among, instead of sending input to all the panes. This helps when we would like some pane to be the monitor of status and other panes run the same command in different directories:
![20210105105938](https://user-images.githubusercontent.com/5045159/103601983-e8ae3200-4f45-11eb-832c-daa3032cf19e.png)


I am also very interested in this feature, When will this feature be released? release plan?

It will be released within a month after someone implements it! We aren’t planning on doing it before version 2.0, however. 
@DHowett can you please specify approximately when v2.0 will be released?
@zljubisic Nope, not really. I'd rather ship 2.0 when it's ready, rather than stick to a firm date. We're hoping for end-of-year-ish, but that's a very soft window. 
@zadjii-msft This broadcast input is the most needed option for every power user.
If you have to configure several servers at once, you cannot live without it.
I really like windows terminal, but when I have to broadcast input, I have to use terminator.
I cannot figure out why implementing such option is a (big) problem.

There are frankly other things that are a higher priority. This issue isn't on [the first page of issues sorted by 👍](https://github.com/microsoft/terminal/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc), which right now is our best metric for judging the relative importance of issues. Don't worry, we _are_ planning on getting to this, we just have some other commitments we have to finish first. 
You want to say that "Add emoji support to Windows Console" is of higher priority? 😁
@zljubisic I serve at the pleasure of the community ☺️ 
😁
@carlos-zamora this is the one I was talking about in team sync
@zadjii-msft 

Alright, pulled up my MacBook and played around with iTerm2's broadcast input feature. Here's my findings:
- Modes:
  - (default) Send input to current session only
    - This is how Windows Terminal works now. You select a pane and any/all input goes to that pane.
  - *broadcast input to all panes in all tabs
    - A warning is displayed. See below.
    - In effect until you toggle it (goes back to default state), or switch states.
    - Still in effect if you switch tabs (makes sense).
  - *broadcast input to all panes in current tab
    - A warning is displayed. See below.
    - In effect until you toggle it (goes back to default state), or switch states.
    - The scope of the effect is limited to the tab. So if you switch tabs, the tab you switched to is in its own separate state. If you come back to the first tab, you'll still be in this broadcast input mode.
  - toggle broadcast input to current session
    - No warning displayed.
    - Toggling broadcast input to a terminal session toggles the active pane icon.
    - Strange interactions with the other modes:
      - If you're in a different broadcast input mode, then you toggle the input mode _for a terminal session specifically_, the active pane icons stay on all the other terminals. Instead, the active pane icon is removed from the current terminal session, but (counterintuitively, IMO) all input goes _only_ to the current terminal session. Toggling broadcast input mode for this session again (enabling it), makes all input go to _all_ active panes again.
- Miscellaneous notes:
  - There seem to be three indicators for a terminal session: the active pane icon, a blinking cursor, and a darker background color (in-focus?).
  - Broadcast input modes mainly update the active pane icons and darker background color for panes that are accepting input.
  - Interestingly, there's only ever _one_ pane with a blinking cursor on the screen. When the broadcast input mode is exited, focus returns to the pane with the blinking cursor.
- Navigating between broadcast input modes
  - modes
    - default
      - option+shift+cmd+I
      - windows equivalent --> alt+shift+ctrl+I
    - all panes & all tabs
      - shift+cmd+I
      - windows equivalent --> shift+ctrl+I
    - all panes in current tab
      - option+cmd+I
      - windows equivalent --> alt+ctrl+I
    - toggle input for current session
      - ctrl+option+shift+cmd+I
      - windows equivalent --> _we don't have enough keys!!!_
  - other invocation methods
    - You can go to the "Shell" menu item, then down to "Broadcast Input". You'll get a list of things you can do (see image submitted by author).
    - All four modes of broadcast input have a keybinding (as described above)
    - You can also right click on a pane and select "Toggle broadcasting input".
    - Windows Terminal's command palette sounds really useful here to me. I personally would love to have all of these broadcast input things be in a nested command item. Then I can just choose the one I want.
    - Windows Terminal would greatly benefit from a context menu here for toggling broadcast input to a specific session.

When entering a mode where input is being broadcast to multiple panes (the modes with a * above), the following warning appears.
<img width="293" alt="Broadcast warning" src="https://user-images.githubusercontent.com/11050425/109692447-65701d80-7b3d-11eb-899f-7a38a666d4b5.png">

If some kind of broadcast input mode is enabled, the affected panes get an icon in the top left:
<img width="720" alt="Pane with active input" src="https://user-images.githubusercontent.com/11050425/109692845-d9122a80-7b3d-11eb-80ff-ef00f66f2d30.png">

<img width="42" alt="Active Pane Icon" src="https://user-images.githubusercontent.com/11050425/109692974-f646f900-7b3d-11eb-95b7-b36737c6a993.png">

The icon looks like Mac's command button and a cute little apple logo.

iTerm2's documentation on broadcast input mode (from [docs site](https://iterm2.com/documentation-one-page.html))
<img width="689" alt="Broadcast Input Mode Documentation" src="https://user-images.githubusercontent.com/11050425/109695131-6a829c00-7b40-11eb-97e1-e682b77bcea1.png">

Let me know if you have any questions. I can always play with this feature more. :)
@carlos-zamora thanks! Can you check one other thing for me? From https://github.com/microsoft/terminal/pull/9222#issuecomment-783539624

> The scenario that gets me is like:
> 1. Enable broadcast input for a pane in tab 1
> 2. switch to tab 2
> 3. execute action C to send input to all the panes in the current tab
> 
> Does that mean that you're now broadcasting to the pane in tab 1 and all the panes in tab 2? If at this point you:
> 
> 4. Switch to tab 3
> 
> now who's getting input? All the panes in tab 3? All the panes in tab 2? All the panes in 2, plus that first pane, plus the active pane in 3?
> 
> @carlos-zamora thanks! Can you check one other thing for me? From [#9222 (comment)](https://github.com/microsoft/terminal/pull/9222#issuecomment-783539624)
> 
> > The scenario that gets me is like:
> > 
> > 1. Enable broadcast input for a pane in tab 1
> > 2. switch to tab 2
> > 3. execute action C to send input to all the panes in the current tab
> > 
> > Does that mean that you're now broadcasting to the pane in tab 1 and all the panes in tab 2? If at this point you:
> > 
> > 1. Switch to tab 3
> > 
> > now who's getting input? All the panes in tab 3? All the panes in tab 2? All the panes in 2, plus that first pane, plus the active pane in 3?

@zadjii-msft Surprisngly, step 3 removes the "toggle broadcast input" icon (or active pane icon, whatever), from tab 1. You are not _only_ broadcasting input to all panes in tab 2.

If you then do step 4, all input now _only_ goes to your active session in tab 3.

I think this is a bit of what I was saying where your broadcast mode is scoped to the tab _unless_ you are broadcasting input to all panes in all tabs. 
Interesting! That's not what I would have expected. Thanks for digging in on all this ☺️ 
I have tried this feature in other terminals and I am really missing it in Windows Terminal :')
I use split planes with Terminator on Linux to broadcast input to all panes within a tab. This is great when I want to login to a bunch of different cloud boxes running the same daemons to check logs, kick services, and do other various things all at once instead of one at a time. I can run it in WSL, but its horribly rendered and I like shiny things on my dual 4K monitors and Windows 11.

Windows Terminal has a lot of potential to convert over engineers currently using Linux and macOS , little features like this that make our lives easier and increase productivity go a long way to making people switch. 

Hit me up if I can test any fun dev builds or give more detailed feedback. 



> I use split planes with Terminator on Linux to broadcast input to all panes within a tab. This is great when I want to login to a bunch of different cloud boxes running the same daemons to check logs, kick services, and do other various things all at once instead of one at a time. I can run it in WSL, but its horribly rendered and I like shiny things on my dual 4K monitors and Windows 11.
> 
> Windows Terminal has a lot of potential to convert over engineers currently using Linux and macOS , little features like this that make our lives easier and increase productivity go a long way to making people switch.
> 
> Hit me up if I can test any fun dev builds or give more detailed feedback.

The more recent work done to Windows Terminal Preview has been absolutely amazing. But for the love of all things good and holy, **please** spend some time on supporting broadcast input! Quite frankly, once we have this, I'm pretty much at feature parity with tools like iterm2, and as someone trying to make the switch to PC from a Mac, WSL2 and Windows Terminal have been a game changer.
The spec is in #9365, we just have a lot of other priorities ahead of this one currently. Feedback is welcome there though!
@zadjii-msft IMHO this is one of the most useful features in Terminator/iterm2 for power users that have to do SSH sessions on multiple servers/instances. For me it's one of the most important feature in a good terminal emulator and I guess a lot of people would highly appreciate this feature being implemented rather sooner then later. IMHO it would be sufficient to just "make it work somehow" for now so we don't have to repeat/copy-paste between terminal windows. You could add the fancy stuff like groups etc. in a later release, but if we could have a base version for now that just broadcasts to all panes in the current tab I would be more than happy.
Not using iTerm2 but I'd be very happy with something that just works like Tmux's `synchronize-panes` feature. Nothing fancy, just send to all panes or not. (https://stackoverflow.com/a/23704472)
On my mac I have a program I wrote that greps systems out of the /etc/hosts, then one by one opens them in tabs in iterm. So, with one command I can open all 20 prod systems. Then I can broadcast the input to all 20 systems to perform the task at hand. The first thing I look for in a terminal app is if it can do tabs. Then can I script the opening of those tabs. Then can I broadcast the input to those tabs. Iterm2 is really the reason I run a mac. Apple should be paying these guys.  If Iterm ran on windows, I'd probably switch over to windows. I mean on my mac outlook, teams, word, powerpoint, excel, onenote, edge, vsCode, iterm are the programs I use most so if Windows had an iterm replacement why run a mac?
IMO, this is the one feature Windows Terminal is missing. I'm currently using the Tmux command in WSL to get this. I often setup multiple servers in HA clusters (for example, I'm currently installing a new Artifactory cluster) and the install process is normally 99% the same inputs. 
This is very much needed, and this is pretty much the only reason that i'm still using SecureCRT.
Just want to chime in that doing this in an iTerm2/Terminator style where one of the windows is "active" is far better than SecureCRT style where there is a "chat window" that broadcasts later because an active window allows you to <TAB> for command-line completion, etc.  
Another vote that this is something that is very much needed.  
> doing this in an iTerm2/Terminator style where one of the windows is "active" is far better than SecureCRT style where there is a "chat window" that broadcasts later

That's exactly how it's spec'd in #9365 😉 
I also use tmux on Linux and it has this functionality built-in and it is quite handy. I have often dreamed to be able to do this with PowerShell and/or Dos (cmd) sessions though. Another advantage which Terminal could bring to the table is that maybe because terminal is a UI it would be possible to select panes with a checkbox and in a layout with e.g. 6 sessions, we select only 4 to be in sync with each other and the other 2 work independently from each other and all the rest, or they sync between the two of them regardless which one is active. The numbers are arbitrary and the functionality should be able to work with completely different division and number of synced pane-groups.
Looking at Konsole -- you have a choice, either send it to all tabs in the
window, or send it to select. Picking select allows you to select which tab
to send the broadcast to. This would be a good feature to emulate with
terminal.

This is definitely needed. Having a suitable iTerm2 replacement is pretty much the only thing keeping my company from switching from macOS to Windows. Seems small to some people but to developers and people that manage hundred of Linux systems, it is pretty much a must have quality of life feature.
I use Terminator inside an Ubuntu Desktop guest instance of VMware Workstation to get this feature. I use the guest instance for other things as well, but this feature is probably one of the main reasons. If Windows Terminal added the ability to do broadcasts to multiple panes and tabs, then that would be great. 

I see that this issue was opened 3 years ago. Is it safe to assume this feature is just NOT going to be added? It's almost as if this is of little-to-no priority to the Windows Terminal developers.  I wish there was a way to make them see that it would be a huge gamechanger if the feature was added, but I digress.
I'm sorry if we've given the impression that this feature isn't important. It absolutely is! I wish we had the resources available to us to get to everything the community asks for faster, but alas, we've got to pick and choose our battles.

We have had some progress on this in the past. There's a draft spec in #9365. There was a community PR a while back in #9222, which was quite promising but got a little too into the weeds for its own good. I don't think it's too far away from the finish line, just needs the diligence to polish it off. 
is this going to introduce in next update ?
> is this going to introduce in next update ?

Nope. This currently isn't assigned to a milestone. Officially, we're still reviewing the spec, but we haven't really bumped that thread in a while. If someone were to come along and try to resurrect #9222, that might force our hand to get our gears in motion. This isn't currently planned for at least a few months.

We'll make sure to update this thread when there's progress. In the meantime, might I recommend the Subscribe button?
![image](https://user-images.githubusercontent.com/18356694/91237459-5cbb0c80-e700-11ea-9347-b9b1ec2813b1.png)
That way you'll be notified of any updates to this thread, without needlessly pinging everyone on this thread ☺️
Hey just as a feeler for folks following in this thread - How much demand is there for broadcasting to a subset of panes across multiple tabs? e.g.

* Tab 1
  * Pane A
  * Pane B
  * Pane C
* Tab 2
  * Pane D
  * Pane E
  * Pane F
 
Are folks looking for the ability to broadcast to [A, E] (for example)?

I'm pretty sure the spec currently covers 
* Broadcast to any subset of a single tab's panes
* Broadcast to all panes in the current tab
* Broadcast to all panes in all tabs

but only Proposal 2 in the spec covers "broadcast to multiple panes in multiple different tabs"
> Hey just as a feeler for folks following in this thread - How much demand is there for broadcasting to a subset of panes across multiple tabs? e.g.


I could see that functionality being useful. It'd allow me to have a tab with a single pane to work on, but it that blurts it out to the next tab that contains several panes.

For me, the MVP is broadcasting to all panes in a single tab. Anything after that is a bonus.
@zadjii-msft I think the current spec is entirely reasonable and is just about on equal terms with the capabilities of other popular terminal emulators. The second proposal sounds interesting from a flexibility standpoint but I think is ultimately unnecessary. If you want to broadcast to two specific panes there's no reason a user can't just create a new tab with only two panes and do their broadcasting in that tab.

I agree with @AlistairHardy, the important one is broadcasting to all panes in the current tab. I would call this behavior "familiar" and as a productivity feature, familiar is good. 
> Hey just as a feeler for folks following in this thread - How much demand is there for broadcasting to a subset of panes across multiple tabs?

I would say that having the option to just do all panes in one tab would be worlds better than nothing at all. At least with that method we would be able to control which panes are in the active tab.
If we could just have broadcast to all panes in one tab in v1 it would be great! Then tinker around the edges as you please. :wink:
> Are folks looking for the ability to broadcast to [A, E] (for example)?

IMHO, even though other Terminal Emulators support this, it could even be dangerous and unexpected as you broadcast to a session that is not visible to you. Personally, I'd rather open more terminals in the current tab and have all the sessions I broadcast input to in my view.

> 
> I'm pretty sure the spec currently covers
> 
>     * Broadcast to any subset of a single tab's panes

From my point of view this is nice to have but not as important as:

>     * Broadcast to all panes in the current tab

This should be Version 1.
 
>     * Broadcast to all panes in all tabs

IMHO not needed at all.
> Hey just as a feeler for folks following in this thread - How much demand is there for broadcasting to a subset of panes across multiple tabs? e.g.
> 
>     * Tab 1
>       
>       * Pane A
>       * Pane B
>       * Pane C
> 
>     * Tab 2
>       
>       * Pane D
>       * Pane E
>       * Pane F
> 
> 
> Are folks looking for the ability to broadcast to [A, E] (for example)?
> 
> I'm pretty sure the spec currently covers
> 
>     * Broadcast to any subset of a single tab's panes
> 
>     * Broadcast to all panes in the current tab
> 
>     * Broadcast to all panes in all tabs
> 
> 
> but only Proposal 2 in the spec covers "broadcast to multiple panes in multiple different tabs"

I would be happy with all panes in a single tab or a subset of panes within a tab.  I don't need all panes in all tabs.  

A subset in the current tab lets me do what I need -- For example, I could start an upgrade in all panes than pause broadcast to all if a subset fail for the same reason and resolve those failures by broadcasting them if necessary. 

I would also be happy if revision 1 gave us all panes in the tab and then the next update added the subset -- just at least get all panes out there!

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 0238b4cd43e..722bf19093b 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -2132,6 +2132,7 @@ WDDMCONSOLECONTEXT
 wdm
 webpage
 websites
+websockets
 wekyb
 wex
 wextest
diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml
index 59613fa02b7..1cda23ddd7c 100644
--- a/src/cascadia/TerminalApp/App.xaml
+++ b/src/cascadia/TerminalApp/App.xaml
@@ -174,6 +174,9 @@
 
                             <SolidColorBrush x:Key="SettingsUiTabBrush"
                                              Color="#0c0c0c" />
+
+                            <SolidColorBrush x:Key="BroadcastPaneBorderColor"
+                                             Color="{StaticResource SystemAccentColorDark2}" />
                         </ResourceDictionary>
 
                         <ResourceDictionary x:Key="Light">
@@ -190,6 +193,9 @@
 
                             <SolidColorBrush x:Key="SettingsUiTabBrush"
                                              Color="#ffffff" />
+
+                            <SolidColorBrush x:Key="BroadcastPaneBorderColor"
+                                             Color="{StaticResource SystemAccentColorLight2}" />
                         </ResourceDictionary>
 
                         <ResourceDictionary x:Key="HighContrast">
@@ -210,6 +216,9 @@
 
                             <StaticResource x:Key="SettingsUiTabBrush"
                                             ResourceKey="SystemControlBackgroundBaseLowBrush" />
+
+                            <SolidColorBrush x:Key="BroadcastPaneBorderColor"
+                                             Color="{StaticResource SystemColorHighlightColor}" />
                         </ResourceDictionary>
 
                     </ResourceDictionary.ThemeDictionaries>
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 74ae4cdbb6a..3b33f8b5897 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -1272,6 +1272,17 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    void TerminalPage::_HandleToggleBroadcastInput(const IInspectable& /*sender*/,
+                                                   const ActionEventArgs& args)
+    {
+        if (const auto activeTab{ _GetFocusedTabImpl() })
+        {
+            activeTab->ToggleBroadcastInput();
+            args.Handled(true);
+        }
+        // If the focused tab wasn't a TerminalTab, then leave handled=false
+    }
+
     void TerminalPage::_HandleRestartConnection(const IInspectable& /*sender*/,
                                                 const ActionEventArgs& args)
     {
@@ -1294,4 +1305,5 @@ namespace winrt::TerminalApp::implementation
         }
         args.Handled(true);
     }
+
 }
diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml
index 7122a750197..c8100e3568c 100644
--- a/src/cascadia/TerminalApp/CommandPalette.xaml
+++ b/src/cascadia/TerminalApp/CommandPalette.xaml
@@ -205,6 +205,13 @@
                                   Glyph="&#xE72E;"
                                   Visibility="{x:Bind Item.(local:TabPaletteItem.TabStatus).IsReadOnlyActive, Mode=OneWay}" />
 
+                        <FontIcon x:Name="HeaderBroadcastIcon"
+                                  Margin="0,0,8,0"
+                                  FontFamily="Segoe MDL2 Assets"
+                                  FontSize="12"
+                                  Glyph="&#xEC05;"
+                                  Visibility="{x:Bind Item.(local:TabPaletteItem.TabStatus).IsInputBroadcastActive, Mode=OneWay}" />
+
                     </StackPanel>
                 </Grid>
             </DataTemplate>
diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp
index 7b85a5f5a62..c70b3fa27da 100644
--- a/src/cascadia/TerminalApp/Pane.cpp
+++ b/src/cascadia/TerminalApp/Pane.cpp
@@ -108,6 +108,7 @@ void Pane::_setupControlEvents()
     _controlEvents._WarningBell = _control.WarningBell(winrt::auto_revoke, { this, &Pane::_ControlWarningBellHandler });
     _controlEvents._CloseTerminalRequested = _control.CloseTerminalRequested(winrt::auto_revoke, { this, &Pane::_CloseTerminalRequestedHandler });
     _controlEvents._RestartTerminalRequested = _control.RestartTerminalRequested(winrt::auto_revoke, { this, &Pane::_RestartTerminalRequestedHandler });
+    _controlEvents._ReadOnlyChanged = _control.ReadOnlyChanged(winrt::auto_revoke, { this, &Pane::_ControlReadOnlyChangedHandler });
 }
 void Pane::_removeControlEvents()
 {
@@ -1434,8 +1435,9 @@ void Pane::UpdateVisuals()
     {
         _UpdateBorders();
     }
-    _borderFirst.BorderBrush(_lastActive ? _themeResources.focusedBorderBrush : _themeResources.unfocusedBorderBrush);
-    _borderSecond.BorderBrush(_lastActive ? _themeResources.focusedBorderBrush : _themeResources.unfocusedBorderBrush);
+    const auto& brush{ _ComputeBorderColor() };
+    _borderFirst.BorderBrush(brush);
+    _borderSecond.BorderBrush(brush);
 }
 
 // Method Description:
@@ -3177,3 +3179,76 @@ void Pane::CollectTaskbarStates(std::vector<winrt::TerminalApp::TaskbarState>& s
         _secondChild->CollectTaskbarStates(states);
     }
 }
+
+void Pane::EnableBroadcast(bool enabled)
+{
+    if (_IsLeaf())
+    {
+        _broadcastEnabled = enabled;
+        UpdateVisuals();
+    }
+    else
+    {
+        _firstChild->EnableBroadcast(enabled);
+        _secondChild->EnableBroadcast(enabled);
+    }
+}
+
+void Pane::BroadcastKey(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl,
+                        const WORD vkey,
+                        const WORD scanCode,
+                        const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers,
+                        const bool keyDown)
+{
+    WalkTree([&](const auto& pane) {
+        if (pane->_IsLeaf() && pane->_control != sourceControl && !pane->_control.ReadOnly())
+        {
+            pane->_control.RawWriteKeyEvent(vkey, scanCode, modifiers, keyDown);
+        }
+    });
+}
+
+void Pane::BroadcastChar(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl,
+                         const wchar_t character,
+                         const WORD scanCode,
+                         const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers)
+{
+    WalkTree([&](const auto& pane) {
+        if (pane->_IsLeaf() && pane->_control != sourceControl && !pane->_control.ReadOnly())
+        {
+            pane->_control.RawWriteChar(character, scanCode, modifiers);
+        }
+    });
+}
+
+void Pane::BroadcastString(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl,
+                           const winrt::hstring& text)
+{
+    WalkTree([&](const auto& pane) {
+        if (pane->_IsLeaf() && pane->_control != sourceControl && !pane->_control.ReadOnly())
+        {
+            pane->_control.RawWriteString(text);
+        }
+    });
+}
+
+void Pane::_ControlReadOnlyChangedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                          const winrt::Windows::Foundation::IInspectable& /*e*/)
+{
+    UpdateVisuals();
+}
+
+winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::_ComputeBorderColor()
+{
+    if (_lastActive)
+    {
+        return _themeResources.focusedBorderBrush;
+    }
+
+    if (_broadcastEnabled && (_IsLeaf() && !_control.ReadOnly()))
+    {
+        return _themeResources.broadcastBorderBrush;
+    }
+
+    return _themeResources.unfocusedBorderBrush;
+}
diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h
index bc3e41a4a23..a6137534dac 100644
--- a/src/cascadia/TerminalApp/Pane.h
+++ b/src/cascadia/TerminalApp/Pane.h
@@ -55,6 +55,7 @@ struct PaneResources
 {
     winrt::Windows::UI::Xaml::Media::SolidColorBrush focusedBorderBrush{ nullptr };
     winrt::Windows::UI::Xaml::Media::SolidColorBrush unfocusedBorderBrush{ nullptr };
+    winrt::Windows::UI::Xaml::Media::SolidColorBrush broadcastBorderBrush{ nullptr };
 };
 
 class Pane : public std::enable_shared_from_this<Pane>
@@ -142,6 +143,11 @@ class Pane : public std::enable_shared_from_this<Pane>
 
     bool ContainsReadOnly() const;
 
+    void EnableBroadcast(bool enabled);
+    void BroadcastKey(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl, const WORD vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown);
+    void BroadcastChar(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl, const wchar_t vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers);
+    void BroadcastString(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl, const winrt::hstring& text);
+
     void UpdateResources(const PaneResources& resources);
 
     // Method Description:
@@ -251,6 +257,7 @@ class Pane : public std::enable_shared_from_this<Pane>
         winrt::Microsoft::Terminal::Control::TermControl::WarningBell_revoker _WarningBell;
         winrt::Microsoft::Terminal::Control::TermControl::CloseTerminalRequested_revoker _CloseTerminalRequested;
         winrt::Microsoft::Terminal::Control::TermControl::RestartTerminalRequested_revoker _RestartTerminalRequested;
+        winrt::Microsoft::Terminal::Control::TermControl::ReadOnlyChanged_revoker _ReadOnlyChanged;
     } _controlEvents;
     void _setupControlEvents();
     void _removeControlEvents();
@@ -263,6 +270,7 @@ class Pane : public std::enable_shared_from_this<Pane>
     Borders _borders{ Borders::None };
 
     bool _zoomed{ false };
+    bool _broadcastEnabled{ false };
 
     winrt::Windows::Media::Playback::MediaPlayer _bellPlayer{ nullptr };
     bool _bellPlayerCreated{ false };
@@ -281,6 +289,7 @@ class Pane : public std::enable_shared_from_this<Pane>
     void _SetupEntranceAnimation();
     void _UpdateBorders();
     Borders _GetCommonBorders();
+    winrt::Windows::UI::Xaml::Media::SolidColorBrush _ComputeBorderColor();
 
     bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::ResizeDirection& direction);
 
@@ -307,6 +316,9 @@ class Pane : public std::enable_shared_from_this<Pane>
                                  const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
     void _ControlLostFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
                                   const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
+
+    void _ControlReadOnlyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& e);
+
     void _CloseTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
     void _RestartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
 
diff --git a/src/cascadia/TerminalApp/TabHeaderControl.xaml b/src/cascadia/TerminalApp/TabHeaderControl.xaml
index 80889eabb8c..5fde709ecbf 100644
--- a/src/cascadia/TerminalApp/TabHeaderControl.xaml
+++ b/src/cascadia/TerminalApp/TabHeaderControl.xaml
@@ -43,6 +43,12 @@
                   FontSize="12"
                   Glyph="&#xE72E;"
                   Visibility="{x:Bind TabStatus.IsReadOnlyActive, Mode=OneWay}" />
+        <FontIcon x:Name="HeaderBroadcastIcon"
+                  Margin="0,0,8,0"
+                  FontFamily="{ThemeResource SymbolThemeFontFamily}"
+                  FontSize="12"
+                  Glyph="&#xEC05;"
+                  Visibility="{x:Bind TabStatus.IsInputBroadcastActive, Mode=OneWay}" />
         <TextBlock x:Name="HeaderTextBlock"
                    Text="{x:Bind Title, Mode=OneWay}"
                    Visibility="Visible" />
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index c0b3f0d5943..b38a0d82daa 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -2787,6 +2787,24 @@ namespace winrt::TerminalApp::implementation
     // - Paste text from the Windows Clipboard to the focused terminal
     void TerminalPage::_PasteText()
     {
+        // First, check if we're in broadcast input mode. If so, let's tell all
+        // the controls to paste.
+        if (const auto& tab{ _GetFocusedTabImpl() })
+        {
+            if (tab->TabStatus().IsInputBroadcastActive())
+            {
+                tab->GetRootPane()->WalkTree([](auto&& pane) {
+                    if (auto control = pane->GetTerminalControl())
+                    {
+                        control.PasteTextFromClipboard();
+                    }
+                });
+                return;
+            }
+        }
+
+        // The focused tab wasn't in broadcast mode. No matter. Just ask the
+        // current one to paste.
         if (const auto& control{ _GetActiveControl() })
         {
             control.PasteTextFromClipboard();
@@ -4570,6 +4588,21 @@ namespace winrt::TerminalApp::implementation
             // will eat focus.
             _paneResources.unfocusedBorderBrush = SolidColorBrush{ Colors::Black() };
         }
+
+        const auto broadcastColorKey = winrt::box_value(L"BroadcastPaneBorderColor");
+        if (res.HasKey(broadcastColorKey))
+        {
+            // MAKE SURE TO USE ThemeLookup
+            auto obj = ThemeLookup(res, requestedTheme, broadcastColorKey);
+            _paneResources.broadcastBorderBrush = obj.try_as<winrt::Windows::UI::Xaml::Media::SolidColorBrush>();
+        }
+        else
+        {
+            // DON'T use Transparent here - if it's "Transparent", then it won't
+            // be able to hittest for clicks, and then clicking on the border
+            // will eat focus.
+            _paneResources.broadcastBorderBrush = SolidColorBrush{ Colors::Black() };
+        }
     }
 
     void TerminalPage::WindowActivated(const bool activated)
diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp
index fe8bee8f81c..ad90c6b1d7b 100644
--- a/src/cascadia/TerminalApp/TerminalTab.cpp
+++ b/src/cascadia/TerminalApp/TerminalTab.cpp
@@ -512,6 +512,8 @@ namespace winrt::TerminalApp::implementation
             }
             return false;
         });
+        pane->EnableBroadcast(_tabStatus.IsInputBroadcastActive());
+
         // Make sure to take the ID before calling Split() - Split() will clear out the active pane's ID
         const auto activePaneId = _activePane->Id();
         // Depending on which direction will be split, the new pane can be
@@ -620,7 +622,7 @@ namespace winrt::TerminalApp::implementation
                 _nextPaneId++;
             }
         });
-
+        pane->EnableBroadcast(_tabStatus.IsInputBroadcastActive());
         // pass the old id to the new child
         const auto previousId = _activePane->Id();
 
@@ -890,6 +892,10 @@ namespace winrt::TerminalApp::implementation
             control.ReadOnlyChanged(events.readOnlyToken);
             control.FocusFollowMouseRequested(events.focusToken);
 
+            events.KeySent.revoke();
+            events.CharSent.revoke();
+            events.StringSent.revoke();
+
             _controlEvents.erase(paneId);
         }
     }
@@ -964,7 +970,12 @@ namespace winrt::TerminalApp::implementation
             }
         });
 
-        _controlEvents[paneId] = events;
+        if (_tabStatus.IsInputBroadcastActive())
+        {
+            _addBroadcastHandlers(control, events);
+        }
+
+        _controlEvents[paneId] = std::move(events);
     }
 
     // Method Description:
@@ -1763,4 +1774,88 @@ namespace winrt::TerminalApp::implementation
 
         return Title();
     }
+
+    // Method Description:
+    // - Toggle broadcasting input to all the panes in this tab.
+    void TerminalTab::ToggleBroadcastInput()
+    {
+        const bool newIsBroadcasting = !_tabStatus.IsInputBroadcastActive();
+        _tabStatus.IsInputBroadcastActive(newIsBroadcasting);
+        _rootPane->EnableBroadcast(newIsBroadcasting);
+
+        auto weakThis{ get_weak() };
+
+        // When we change the state of broadcasting, add or remove event
+        // handlers appropriately, so that controls won't be propagating events
+        // needlessly if no one is listening.
+
+        _rootPane->WalkTree([&](const auto& p) {
+            const auto paneId = p->Id();
+            if (!paneId.has_value())
+            {
+                return;
+            }
+            if (const auto& control{ p->GetTerminalControl() })
+            {
+                auto it = _controlEvents.find(*paneId);
+                if (it != _controlEvents.end())
+                {
+                    auto& events = it->second;
+
+                    // Always clear out old ones, just in case.
+                    events.KeySent.revoke();
+                    events.CharSent.revoke();
+                    events.StringSent.revoke();
+
+                    if (newIsBroadcasting)
+                    {
+                        _addBroadcastHandlers(control, events);
+                    }
+                }
+            }
+        });
+    }
+
+    void TerminalTab::_addBroadcastHandlers(const TermControl& termControl, ControlEventTokens& events)
+    {
+        auto weakThis{ get_weak() };
+        // ADD EVENT HANDLERS HERE
+        events.KeySent = termControl.KeySent(winrt::auto_revoke, [weakThis](auto&& sender, auto&& e) {
+            if (const auto tab{ weakThis.get() })
+            {
+                if (tab->_tabStatus.IsInputBroadcastActive())
+                {
+                    tab->_rootPane->BroadcastKey(sender.try_as<TermControl>(),
+                                                 e.VKey(),
+                                                 e.ScanCode(),
+                                                 e.Modifiers(),
+                                                 e.KeyDown());
+                }
+            }
+        });
+
+        events.CharSent = termControl.CharSent(winrt::auto_revoke, [weakThis](auto&& sender, auto&& e) {
+            if (const auto tab{ weakThis.get() })
+            {
+                if (tab->_tabStatus.IsInputBroadcastActive())
+                {
+                    tab->_rootPane->BroadcastChar(sender.try_as<TermControl>(),
+                                                  e.Character(),
+                                                  e.ScanCode(),
+                                                  e.Modifiers());
+                }
+            }
+        });
+
+        events.StringSent = termControl.StringSent(winrt::auto_revoke, [weakThis](auto&& sender, auto&& e) {
+            if (const auto tab{ weakThis.get() })
+            {
+                if (tab->_tabStatus.IsInputBroadcastActive())
+                {
+                    tab->_rootPane->BroadcastString(sender.try_as<TermControl>(),
+                                                    e.Text());
+                }
+            }
+        });
+    }
 }
diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h
index e2a225e2c63..d7c1cede718 100644
--- a/src/cascadia/TerminalApp/TerminalTab.h
+++ b/src/cascadia/TerminalApp/TerminalTab.h
@@ -85,6 +85,7 @@ namespace winrt::TerminalApp::implementation
 
         void TogglePaneReadOnly();
         void SetPaneReadOnly(const bool readOnlyState);
+        void ToggleBroadcastInput();
 
         std::shared_ptr<Pane> GetActivePane() const;
         winrt::TerminalApp::TaskbarState GetCombinedTaskbarState() const;
@@ -133,6 +134,10 @@ namespace winrt::TerminalApp::implementation
             winrt::event_token taskbarToken;
             winrt::event_token readOnlyToken;
             winrt::event_token focusToken;
+
+            winrt::Microsoft::Terminal::Control::TermControl::KeySent_revoker KeySent;
+            winrt::Microsoft::Terminal::Control::TermControl::CharSent_revoker CharSent;
+            winrt::Microsoft::Terminal::Control::TermControl::StringSent_revoker StringSent;
         };
         std::unordered_map<uint32_t, ControlEventTokens> _controlEvents;
 
@@ -179,6 +184,8 @@ namespace winrt::TerminalApp::implementation
 
         virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() override;
 
+        void _addBroadcastHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control, ControlEventTokens& events);
+
         friend class ::TerminalAppLocalTests::TabTests;
     };
 }
diff --git a/src/cascadia/TerminalApp/TerminalTabStatus.h b/src/cascadia/TerminalApp/TerminalTabStatus.h
index d25341829ef..95080935a38 100644
--- a/src/cascadia/TerminalApp/TerminalTabStatus.h
+++ b/src/cascadia/TerminalApp/TerminalTabStatus.h
@@ -18,6 +18,7 @@ namespace winrt::TerminalApp::implementation
         WINRT_OBSERVABLE_PROPERTY(bool, BellIndicator, _PropertyChangedHandlers);
         WINRT_OBSERVABLE_PROPERTY(bool, IsReadOnlyActive, _PropertyChangedHandlers);
         WINRT_OBSERVABLE_PROPERTY(uint32_t, ProgressValue, _PropertyChangedHandlers);
+        WINRT_OBSERVABLE_PROPERTY(bool, IsInputBroadcastActive, _PropertyChangedHandlers);
     };
 }
 
diff --git a/src/cascadia/TerminalApp/TerminalTabStatus.idl b/src/cascadia/TerminalApp/TerminalTabStatus.idl
index 11d659f745f..57f4e05fa1a 100644
--- a/src/cascadia/TerminalApp/TerminalTabStatus.idl
+++ b/src/cascadia/TerminalApp/TerminalTabStatus.idl
@@ -13,5 +13,6 @@ namespace TerminalApp
         Boolean BellIndicator { get; set; };
         UInt32 ProgressValue { get; set; };
         Boolean IsReadOnlyActive { get; set; };
+        Boolean IsInputBroadcastActive { get; set; };
     }
 }
diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp
index c4089ebd76c..12739e38867 100644
--- a/src/cascadia/TerminalControl/EventArgs.cpp
+++ b/src/cascadia/TerminalControl/EventArgs.cpp
@@ -15,3 +15,6 @@
 #include "FoundResultsArgs.g.cpp"
 #include "ShowWindowArgs.g.cpp"
 #include "UpdateSelectionMarkersEventArgs.g.cpp"
+#include "KeySentEventArgs.g.cpp"
+#include "CharSentEventArgs.g.cpp"
+#include "StringSentEventArgs.g.cpp"
diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h
index e1586e600a3..d3588eea672 100644
--- a/src/cascadia/TerminalControl/EventArgs.h
+++ b/src/cascadia/TerminalControl/EventArgs.h
@@ -15,6 +15,9 @@
 #include "FoundResultsArgs.g.h"
 #include "ShowWindowArgs.g.h"
 #include "UpdateSelectionMarkersEventArgs.g.h"
+#include "KeySentEventArgs.g.h"
+#include "CharSentEventArgs.g.h"
+#include "StringSentEventArgs.g.h"
 
 namespace winrt::Microsoft::Terminal::Control::implementation
 {
@@ -179,6 +182,43 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         WINRT_PROPERTY(bool, ClearMarkers, false);
     };
+
+    struct KeySentEventArgs : public KeySentEventArgsT<KeySentEventArgs>
+    {
+    public:
+        KeySentEventArgs(const WORD vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown) :
+            _VKey(vkey),
+            _ScanCode(scanCode),
+            _Modifiers(modifiers),
+            _KeyDown(keyDown) {}
+
+        WINRT_PROPERTY(WORD, VKey);
+        WINRT_PROPERTY(WORD, ScanCode);
+        WINRT_PROPERTY(winrt::Microsoft::Terminal::Core::ControlKeyStates, Modifiers);
+        WINRT_PROPERTY(bool, KeyDown, false);
+    };
+
+    struct CharSentEventArgs : public CharSentEventArgsT<CharSentEventArgs>
+    {
+    public:
+        CharSentEventArgs(const wchar_t character, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers) :
+            _Character(character),
+            _ScanCode(scanCode),
+            _Modifiers(modifiers) {}
+
+        WINRT_PROPERTY(wchar_t, Character);
+        WINRT_PROPERTY(WORD, ScanCode);
+        WINRT_PROPERTY(winrt::Microsoft::Terminal::Core::ControlKeyStates, Modifiers);
+    };
+
+    struct StringSentEventArgs : public StringSentEventArgsT<StringSentEventArgs>
+    {
+    public:
+        StringSentEventArgs(const winrt::hstring& text) :
+            _Text(text) {}
+
+        WINRT_PROPERTY(winrt::hstring, Text);
+    };
 }
 
 namespace winrt::Microsoft::Terminal::Control::factory_implementation
diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl
index 7dbe9d47e47..1b07517b32d 100644
--- a/src/cascadia/TerminalControl/EventArgs.idl
+++ b/src/cascadia/TerminalControl/EventArgs.idl
@@ -89,4 +89,24 @@ namespace Microsoft.Terminal.Control
     {
         Boolean ClearMarkers { get; };
     }
+
+    runtimeclass KeySentEventArgs
+    {
+        UInt16 VKey { get; };
+        UInt16 ScanCode { get; };
+        Microsoft.Terminal.Core.ControlKeyStates Modifiers { get; };
+        Boolean KeyDown { get; };
+    }
+
+    runtimeclass CharSentEventArgs
+    {
+        Char Character { get; };
+        UInt16 ScanCode { get; };
+        Microsoft.Terminal.Core.ControlKeyStates Modifiers { get; };
+    }
+
+    runtimeclass StringSentEventArgs
+    {
+        String Text { get; };
+    }
 }
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 257c7df9251..aefe057a635 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -524,7 +524,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // - <none>
     void TermControl::SendInput(const winrt::hstring& wstr)
     {
-        _core.SendInput(wstr);
+        // only broadcast if there's an actual listener. Saves the overhead of some object creation.
+        if (_StringSentHandlers)
+        {
+            _StringSentHandlers(*this, winrt::make<StringSentEventArgs>(wstr));
+        }
+
+        RawWriteString(wstr);
     }
     void TermControl::ClearBuffer(Control::ClearBufferType clearType)
     {
@@ -1083,10 +1089,31 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             modifiers |= ControlKeyStates::EnhancedKey;
         }
 
-        const auto handled = _core.SendCharEvent(ch, scanCode, modifiers);
+        // Broadcast the character to all listeners
+        // only broadcast if there's an actual listener. Saves the overhead of some object creation.
+        if (_CharSentHandlers)
+        {
+            auto charSentArgs = winrt::make<CharSentEventArgs>(ch, scanCode, modifiers);
+            _CharSentHandlers(*this, charSentArgs);
+        }
+
+        const auto handled = RawWriteChar(ch, scanCode, modifiers);
+
         e.Handled(handled);
     }
 
+    bool TermControl::RawWriteChar(const wchar_t character,
+                                   const WORD scanCode,
+                                   const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers)
+    {
+        return _core.SendCharEvent(character, scanCode, modifiers);
+    }
+
+    void TermControl::RawWriteString(const winrt::hstring& text)
+    {
+        _core.SendInput(text);
+    }
+
     // Method Description:
     // - Manually handles key events for certain keys that can't be passed to us
     //   normally. Namely, the keys we're concerned with are F7 down and Alt up.
@@ -1333,6 +1360,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                        const WORD scanCode,
                                        const ControlKeyStates modifiers,
                                        const bool keyDown)
+    {
+        // Broadcast the key  to all listeners
+        // only broadcast if there's an actual listener. Saves the overhead of some object creation.
+        if (_KeySentHandlers)
+        {
+            auto keySentArgs = winrt::make<KeySentEventArgs>(vkey, scanCode, modifiers, keyDown);
+            _KeySentHandlers(*this, keySentArgs);
+        }
+
+        return RawWriteKeyEvent(vkey, scanCode, modifiers, keyDown);
+    }
+
+    bool TermControl::RawWriteKeyEvent(const WORD vkey,
+                                       const WORD scanCode,
+                                       const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers,
+                                       const bool keyDown)
     {
         const auto window = CoreWindow::GetForCurrentThread();
 
@@ -2587,7 +2630,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             return;
         }
 
-        _core.SendInput(text);
+        // SendInput will take care of broadcasting for us.
+        SendInput(text);
     }
 
     // Method Description:
@@ -2666,7 +2710,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             try
             {
                 auto link{ co_await e.DataView().GetApplicationLinkAsync() };
-                _core.PasteText(link.AbsoluteUri());
+                _pasteTextWithBroadcast(link.AbsoluteUri());
             }
             CATCH_LOG();
         }
@@ -2675,7 +2719,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             try
             {
                 auto link{ co_await e.DataView().GetWebLinkAsync() };
-                _core.PasteText(link.AbsoluteUri());
+                _pasteTextWithBroadcast(link.AbsoluteUri());
             }
             CATCH_LOG();
         }
@@ -2684,7 +2728,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             try
             {
                 auto text{ co_await e.DataView().GetTextAsync() };
-                _core.PasteText(text);
+                _pasteTextWithBroadcast(text);
             }
             CATCH_LOG();
         }
@@ -2812,11 +2856,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                     allPathsString += fullPath;
                 }
 
-                _core.PasteText(winrt::hstring{ allPathsString });
+                _pasteTextWithBroadcast(winrt::hstring{ allPathsString });
             }
         }
     }
 
+    // Method Description:
+    // - Paste this text, and raise a StringSent, to potentially broadcast this
+    //   text to other controls in the app. For certain interactions, like
+    //   drag/dropping a file, we want to act like we "pasted" the text (even if
+    //   the text didn't come from the clipboard). This lets those interactions
+    //   broadcast as well.
+    void TermControl::_pasteTextWithBroadcast(const winrt::hstring& text)
+    {
+        // only broadcast if there's an actual listener. Saves the overhead of some object creation.
+        if (_StringSentHandlers)
+        {
+            _StringSentHandlers(*this, winrt::make<StringSentEventArgs>(text));
+        }
+        _core.PasteText(text);
+    }
+
     // Method Description:
     // - Handle the DragOver event. We'll signal that the drag operation we
     //   support is the "copy" operation, and we'll also customize the
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 827c0d29a4e..01f86a409aa 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -146,6 +146,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         void AdjustOpacity(const double opacity, const bool relative);
 
+        bool RawWriteKeyEvent(const WORD vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown);
+        bool RawWriteChar(const wchar_t character, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers);
+        void RawWriteString(const winrt::hstring& text);
+
         void ShowContextMenu();
 
         void Detach();
@@ -180,7 +184,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         TYPED_EVENT(FocusFollowMouseRequested, IInspectable, IInspectable);
         TYPED_EVENT(Initialized,               Control::TermControl, Windows::UI::Xaml::RoutedEventArgs);
         TYPED_EVENT(WarningBell,               IInspectable, IInspectable);
-
+        TYPED_EVENT(KeySent,                   IInspectable, Control::KeySentEventArgs);
+        TYPED_EVENT(CharSent,                  IInspectable, Control::CharSentEventArgs);
+        TYPED_EVENT(StringSent,                IInspectable, Control::StringSentEventArgs);
         // clang-format on
 
         WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, BackgroundBrush, _PropertyChangedHandlers, nullptr);
@@ -355,6 +361,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         til::point _toPosInDips(const Core::Point terminalCellPos);
         void _throttledUpdateScrollbar(const ScrollBarUpdate& update);
 
+        void _pasteTextWithBroadcast(const winrt::hstring& text);
+
         void _contextMenuHandler(IInspectable sender, Control::ContextMenuRequestedEventArgs args);
         void _showContextMenuAt(const til::point& controlRelativePos);
 
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index b52cfea794b..ad881f233f9 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -53,6 +53,9 @@ namespace Microsoft.Terminal.Control
         event Windows.Foundation.TypedEventHandler<Object, Object> TabColorChanged;
         event Windows.Foundation.TypedEventHandler<Object, Object> ReadOnlyChanged;
         event Windows.Foundation.TypedEventHandler<Object, Object> FocusFollowMouseRequested;
+        event Windows.Foundation.TypedEventHandler<Object, KeySentEventArgs> KeySent;
+        event Windows.Foundation.TypedEventHandler<Object, CharSentEventArgs> CharSent;
+        event Windows.Foundation.TypedEventHandler<Object, StringSentEventArgs> StringSent;
 
         Microsoft.UI.Xaml.Controls.CommandBarFlyout ContextMenu { get; };
         Microsoft.UI.Xaml.Controls.CommandBarFlyout SelectionContextMenu { get; };
@@ -94,6 +97,9 @@ namespace Microsoft.Terminal.Control
 
         void ToggleShaderEffects();
         void SendInput(String input);
+        Boolean RawWriteKeyEvent(UInt16 vkey, UInt16 scanCode, Microsoft.Terminal.Core.ControlKeyStates modifiers, Boolean keyDown);
+        Boolean RawWriteChar(Char character, UInt16 scanCode, Microsoft.Terminal.Core.ControlKeyStates modifiers);
+        void RawWriteString(String text);
 
         void BellLightOn();
 
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index 75ba4487b0c..757b440c82c 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -93,6 +93,7 @@ static constexpr std::string_view ColorSelectionKey{ "experimental.colorSelectio
 static constexpr std::string_view ShowContextMenuKey{ "showContextMenu" };
 static constexpr std::string_view ExpandSelectionToWordKey{ "expandSelectionToWord" };
 static constexpr std::string_view RestartConnectionKey{ "restartConnection" };
+static constexpr std::string_view ToggleBroadcastInputKey{ "toggleBroadcastInput" };
 
 static constexpr std::string_view ActionKey{ "action" };
 
@@ -424,6 +425,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                 { ShortcutAction::ShowContextMenu, RS_(L"ShowContextMenuCommandKey") },
                 { ShortcutAction::ExpandSelectionToWord, RS_(L"ExpandSelectionToWordCommandKey") },
                 { ShortcutAction::RestartConnection, RS_(L"RestartConnectionKey") },
+                { ShortcutAction::ToggleBroadcastInput, RS_(L"ToggleBroadcastInputCommandKey") },
             };
         }();
 
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index 127b0bf2df9..571c20632a4 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -106,7 +106,8 @@
     ON_ALL_ACTIONS(ShowContextMenu)         \
     ON_ALL_ACTIONS(ExpandSelectionToWord)   \
     ON_ALL_ACTIONS(CloseOtherPanes)         \
-    ON_ALL_ACTIONS(RestartConnection)
+    ON_ALL_ACTIONS(RestartConnection)       \
+    ON_ALL_ACTIONS(ToggleBroadcastInput)
 
 #define ALL_SHORTCUT_ACTIONS_WITH_ARGS             \
     ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize)       \
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index 414783c6557..1cd8b5d51f5 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -684,6 +684,10 @@
   <data name="CloseOtherPanesCommandKey" xml:space="preserve">
     <value>Close all other panes</value>
   </data>
+  <data name="ToggleBroadcastInputCommandKey" xml:space="preserve">
+    <value>Toggle broadcast input to all panes</value>
+    <comment>When enabled, input will go to all panes in this tab simultaneously</comment>
+  </data>
   <data name="RestartConnectionKey" xml:space="preserve">
     <value>Restart connection</value>
   </data>


Test patch: