diff --git a/OpenConsole.slnx b/OpenConsole.slnx
index 9dff529afa7..6482d3d340d 100644
--- a/OpenConsole.slnx
+++ b/OpenConsole.slnx
@@ -449,6 +449,7 @@
+
@@ -1046,4 +1047,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md
new file mode 100644
index 00000000000..0f5b79bec90
--- /dev/null
+++ b/doc/wtcli-commands.md
@@ -0,0 +1,40 @@
+# wtcli Command Reference
+
+`wtcli` is the CLI client for the Windows Terminal Protocol. It calls
+`CoCreateInstance` using the per-brand `CLSID` and exposes a tmux-style command surface over its IDL methods.
+
+- Source: `src/tools/wtcli/main.cpp`
+- IDL: `src/host/proxy/ITerminalProtocol.idl`
+- Primary in-tree caller: `tools/wta/src/shell/wt_channel/cli_channel.rs`.
+
+## Global flags
+
+| Flag | Effect |
+|------|--------|
+| `--json` | Emit machine-readable JSON. Required for any caller that parses output. |
+
+## Commands
+
+The "Used in repo" column reflects whether some other component in this
+repository actually shells out to that subcommand today (not whether the
+subcommand is reachable). External callers (third-party agents, ad-hoc
+scripts) are not counted.
+
+| Command | Alias | What it does | Example | Used in repo |
+|---------|-------|--------------|---------|--------------|
+| `list-windows` | `lsw` | List all Terminal windows. | `wtcli --json list-windows` | ✅ `cli_channel.rs` (`list_windows`) |
+| `list-tabs` | `lst` | List tabs in a window. `-w` defaults to the first window. | `wtcli --json list-tabs -w 1` | ✅ `cli_channel.rs` (`list_tabs`) |
+| `list-panes` | `lsp` | List panes in a tab. `-t`/`-w` default to the first tab of the first window. | `wtcli --json list-panes -t 2` | ✅ `cli_channel.rs` (`list_panes`) |
+| `active-pane` | — | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json active-pane` | ✅ `cli_channel.rs` (`get_active_pane`) |
+| `capture-pane` | `capturep` | Read pane scrollback as text. `-l` caps line count. `--last-prompt` returns only the most recent completed shell prompt (requires OSC 133 shell integration). | `wtcli --json capture-pane -t 3 --last-prompt` | ✅ `cli_channel.rs` (`read_pane_output`) |
+| `pane-status` | — | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | ✅ `cli_channel.rs` (`get_process_status`) |
+| `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | ✅ `cli_channel.rs` (`create_tab`) |
+| `split-pane` | `splitw` | Split a pane. `-d right\|left\|up\|down\|auto` (default `automatic`). `-H`/`-v` are legacy aliases for `down`/`right`. `-s` is size fraction; `-c` is the command to run. | `wtcli --json split-pane -t 3 -d right -s 0.4 -c "tail -f log"` | ✅ `cli_channel.rs` (`split_pane`) |
+| `kill-pane` | `killp` | Close a pane. | `wtcli kill-pane -t 4` | ✅ `cli_channel.rs` (`close_pane`) |
+| `focus-pane` | `focusp` | Move focus to the given pane. | `wtcli focus-pane -t 3` | ✅ `cli_channel.rs` (`focus_pane`) |
+
+## Summary
+
+- **Wired into `wta` runtime (10):** `list-windows`, `list-tabs`,
+ `list-panes`, `active-pane`, `capture-pane`, `pane-status`,
+ `new-tab`, `split-pane`, `kill-pane`, `focus-pane`.
\ No newline at end of file
diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
index 317109ed354..61ad620cee9 100644
--- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
+++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
@@ -92,6 +92,9 @@
{71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}
+
+ {8A3B5E2F-1C4D-4F6A-9B8C-0D1E2F3A4B5C}
+
diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest
index 8335a59f420..423c1081f1c 100644
--- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest
@@ -113,6 +113,7 @@
+
@@ -122,6 +123,7 @@
+
diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest
index 4a3153076c8..70ef00e1432 100644
--- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest
@@ -113,6 +113,7 @@
+
@@ -122,6 +123,7 @@
+
diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest
index 497923da40a..7109b7e8ae5 100644
--- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest
@@ -202,6 +202,7 @@
+
@@ -211,6 +212,7 @@
+
diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest
index 679f3b9a526..2aae6d1e049 100644
--- a/src/cascadia/CascadiaPackage/Package.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package.appxmanifest
@@ -202,6 +202,7 @@
+
@@ -211,6 +212,7 @@
+
diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
index 371dbd1746e..04826fa67c1 100644
--- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
+++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
@@ -213,6 +213,10 @@
TerminalPage.xaml
Code
+
+ TerminalPage.xaml
+ Code
+
TerminalPage.xaml
Code
@@ -385,6 +389,10 @@
you also update all the consumers
-->
+
+ {F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5D}
+ false
+
{CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}
false
@@ -467,6 +475,12 @@
false
false
+
+ $(OpenConsoleCommonOutDir)TerminalProtocol\Microsoft.Terminal.Protocol.winmd
+ true
+ false
+ false
+
$(OpenConsoleCommonOutDir)Microsoft.Terminal.UI.Markdown\Microsoft.Terminal.UI.Markdown.winmd
true
diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp
new file mode 100644
index 00000000000..4b1a47f4dc7
--- /dev/null
+++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp
@@ -0,0 +1,648 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+// This file contains the protocol bridge methods for TerminalPage.
+// These methods are called by the TerminalProtocolComServer to query
+// and mutate terminal state.
+//
+// IMPORTANT: These methods are called from background threads (COM).
+// All access to UI state must be marshaled to the UI thread via Dispatcher().
+// Each method is a direct coroutine that uses co_await to switch threads.
+// The ComServer calls .get() on the returned IAsyncOperation to block.
+
+#include "pch.h"
+#include "TerminalPage.h"
+#include "../../types/inc/utils.hpp"
+#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"
+
+#include
+#include "../TerminalProtocol/ProtocolParsing.h"
+
+namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing;
+
+using namespace winrt;
+using namespace winrt::Windows::Foundation;
+using namespace winrt::Windows::UI::Core;
+using namespace winrt::Microsoft::Terminal;
+using namespace winrt::Microsoft::Terminal::Control;
+using namespace winrt::Microsoft::Terminal::TerminalConnection;
+using namespace winrt::Microsoft::Terminal::Settings::Model;
+namespace Protocol = winrt::Microsoft::Terminal::Protocol;
+
+namespace winrt::TerminalApp::implementation
+{
+ // Helper to get PID from a pane's terminal control connection.
+ static uint32_t _getPidFromPane(const std::shared_ptr& pane)
+ {
+ if (const auto termControl = pane->GetTerminalControl())
+ {
+ const auto conn = termControl.Connection();
+ if (conn)
+ {
+ if (const auto conpty = conn.try_as())
+ {
+ const auto handle = conpty.RootProcessHandle();
+ if (handle)
+ {
+ return static_cast(GetProcessId(reinterpret_cast(handle)));
+ }
+ }
+ }
+ }
+ return 0;
+ }
+
+ // Get the connection SessionId for a terminal pane, or empty guid for non-terminal panes.
+ static winrt::guid _getSessionIdFromPane(const std::shared_ptr& pane)
+ {
+ if (const auto termContent = pane->GetContent().try_as())
+ {
+ if (const auto control = termContent.GetTermControl())
+ {
+ if (const auto conn = control.Connection())
+ {
+ return conn.SessionId();
+ }
+ }
+ }
+ return {};
+ }
+
+ // Find a pane by SessionId across all tabs using WalkTree.
+ static std::shared_ptr _findPaneBySessionId(const std::shared_ptr& rootPane, winrt::guid sessionId)
+ {
+ std::shared_ptr found;
+ rootPane->WalkTree([&](const auto& pane) {
+ if (_getSessionIdFromPane(pane) == sessionId)
+ found = pane;
+ });
+ return found;
+ }
+
+ uint32_t TerminalPage::TabCount() const
+ {
+ return [this]() -> IAsyncOperation {
+ co_await wil::resume_foreground(Dispatcher());
+ co_return NumberOfTabs();
+ }().get();
+ }
+
+ Windows::Foundation::IReference TerminalPage::FocusedTabIndex() const
+ {
+ return [this]() -> IAsyncOperation> {
+ co_await wil::resume_foreground(Dispatcher());
+ const auto idx = _GetFocusedTabIndex();
+ if (idx.has_value())
+ {
+ co_return Windows::Foundation::IReference(idx.value());
+ }
+ co_return nullptr;
+ }().get();
+ }
+
+ // ============================================================================
+ // Queries — return typed WinRT structs
+ // ============================================================================
+
+ IAsyncOperation TerminalPage::GetProtocolActivePane()
+ {
+ auto strong = get_strong();
+ co_await wil::resume_foreground(Dispatcher());
+
+ Protocol::PaneInfo result{};
+
+ const auto focusedTabIdx = _GetFocusedTabIndex();
+ if (!focusedTabIdx.has_value())
+ co_return result;
+
+ const auto tab = _tabs.GetAt(focusedTabIdx.value());
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ co_return result;
+
+ const auto activePane = tabImpl->GetActivePane();
+ if (!activePane)
+ co_return result;
+
+ result.SessionId = _getSessionIdFromPane(activePane);
+ result.TabId = focusedTabIdx.value();
+ result.IsActive = true;
+ result.IsAgentPane = false;
+
+ if (const auto termContent = activePane->GetContent().try_as())
+ {
+ result.Title = termContent.Title();
+ const auto profile = termContent.GetProfile();
+ result.Profile = profile ? profile.Name() : L"";
+ }
+
+ if (const auto termControl = activePane->GetTerminalControl())
+ {
+ result.Cwd = termControl.WorkingDirectory();
+ }
+
+ result.Pid = _getPidFromPane(activePane);
+ co_return result;
+ }
+
+ IAsyncOperation> TerminalPage::GetProtocolTabs()
+ {
+ auto strong = get_strong();
+ co_await wil::resume_foreground(Dispatcher());
+
+ auto tabs = winrt::single_threaded_vector();
+ const auto focusedIdx = _GetFocusedTabIndex();
+
+ for (uint32_t i = 0; i < _tabs.Size(); ++i)
+ {
+ const auto tab = _tabs.GetAt(i);
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ Protocol::TabInfo info{};
+ info.TabId = i;
+ info.Title = tab.Title();
+ info.IsActive = focusedIdx.has_value() && (focusedIdx.value() == i);
+ // Count terminal panes only (those with a SessionId).
+ uint32_t terminalPaneCount = 0;
+ if (const auto rootPane = tabImpl->GetRootPane())
+ {
+ rootPane->WalkTree([&](const auto& pane) {
+ if (_getSessionIdFromPane(pane) != winrt::guid{})
+ terminalPaneCount++;
+ });
+ }
+ info.PaneCount = terminalPaneCount;
+ tabs.Append(info);
+ }
+
+ co_return tabs;
+ }
+
+ IAsyncOperation> TerminalPage::GetProtocolPanes(uint32_t tabIdFilter)
+ {
+ auto strong = get_strong();
+
+ co_await wil::resume_foreground(Dispatcher());
+
+ auto panes = winrt::single_threaded_vector();
+
+ for (uint32_t tabIdx = 0; tabIdx < _tabs.Size(); ++tabIdx)
+ {
+ if (tabIdFilter != UINT32_MAX && tabIdx != tabIdFilter)
+ continue;
+
+ const auto tab = _tabs.GetAt(tabIdx);
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ const auto rootPane = tabImpl->GetRootPane();
+ if (!rootPane)
+ continue;
+
+ const auto activePane = tabImpl->GetActivePane();
+
+ rootPane->WalkTree([&](const auto& pane) {
+ if (!pane->GetContent())
+ return; // Skip branch nodes
+
+ const auto sid = _getSessionIdFromPane(pane);
+ if (sid == winrt::guid{})
+ return; // Skip non-terminal panes
+
+ Protocol::PaneInfo info{};
+ info.SessionId = sid;
+ info.TabId = tabIdx;
+ info.IsAgentPane = false;
+ info.IsActive = (activePane == pane);
+ info.Pid = _getPidFromPane(pane);
+
+ if (const auto termContent = pane->GetContent().try_as())
+ {
+ info.Title = termContent.Title();
+ const auto profile = termContent.GetProfile();
+ info.Profile = profile ? profile.Name() : L"";
+
+ if (const auto termControl = pane->GetTerminalControl())
+ {
+ info.Rows = termControl.ViewHeight();
+ info.Columns = 0;
+ info.Cwd = termControl.WorkingDirectory();
+ }
+ }
+
+ panes.Append(info);
+ });
+ }
+
+ co_return panes;
+ }
+
+ IAsyncOperation TerminalPage::ReadProtocolPaneOutput(winrt::guid sessionId, hstring source, int32_t maxLines)
+ {
+ auto strong = get_strong();
+ const auto sourceStr = winrt::to_string(source);
+ const auto sourceRoute = ProtocolParsing::ClassifyPaneOutputSource(sourceStr);
+ const auto effectiveMaxLines = (maxLines <= 0) ? 200 : maxLines;
+
+ co_await wil::resume_foreground(Dispatcher());
+
+ Protocol::PaneOutput result{};
+
+ // UI-thread work: find pane, read buffer.
+ hstring fullBuffer;
+ int32_t viewHeight = 0;
+ for (const auto& tab : _tabs)
+ {
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ const auto rootPane = tabImpl->GetRootPane();
+ if (!rootPane)
+ continue;
+
+ const auto foundPane = _findPaneBySessionId(rootPane, sessionId);
+ if (!foundPane)
+ continue;
+
+ const auto termControl = foundPane->GetTerminalControl();
+ if (!termControl)
+ co_return result; // empty SessionId signals not-ready
+
+ try
+ {
+ if (sourceRoute == ProtocolParsing::PaneOutputSource::LastPrompt)
+ {
+ result.SessionId = sessionId;
+ const auto lastPrompt = termControl.ReadLastPrompt();
+ auto lastPromptStr = winrt::to_string(lastPrompt);
+ if (lastPromptStr.empty())
+ {
+ result.HasMarks = false;
+ result.Content = L"";
+ result.LineCount = 0;
+ result.Truncated = false;
+ co_return result;
+ }
+ int32_t lineCount = 1;
+ for (auto ch : lastPromptStr)
+ {
+ if (ch == '\n')
+ ++lineCount;
+ }
+ result.HasMarks = true;
+ result.Content = winrt::to_hstring(lastPromptStr);
+ result.LineCount = lineCount;
+ result.Truncated = false;
+ co_return result;
+ }
+
+ fullBuffer = termControl.ReadEntireBuffer();
+ viewHeight = termControl.ViewHeight();
+ }
+ catch (...)
+ {
+ co_return result; // empty SessionId signals error
+ }
+
+ result.SessionId = sessionId;
+ break;
+ }
+
+ if (result.SessionId == winrt::guid{})
+ co_return result; // not found
+
+ // Move off UI thread for string processing.
+ co_await winrt::resume_background();
+
+ auto fullBufferStr = winrt::to_string(fullBuffer);
+ std::vector lines;
+ std::istringstream iss(fullBufferStr);
+ std::string line;
+ while (std::getline(iss, line))
+ {
+ if (!line.empty() && line.back() == '\r')
+ line.pop_back();
+ lines.push_back(line);
+ }
+
+ if (sourceRoute == ProtocolParsing::PaneOutputSource::Screen)
+ {
+ const auto startIdx = lines.size() > static_cast(viewHeight)
+ ? lines.size() - viewHeight
+ : 0;
+
+ std::string content;
+ int lineCount = 0;
+ for (size_t i = startIdx; i < lines.size(); ++i)
+ {
+ if (!content.empty())
+ content += "\n";
+ content += lines[i];
+ lineCount++;
+ }
+
+ result.Content = winrt::to_hstring(content);
+ result.LineCount = lineCount;
+ result.Truncated = false;
+ }
+ else
+ {
+ const auto truncated = (static_cast(lines.size()) > effectiveMaxLines);
+ const auto startIdx = truncated ? lines.size() - effectiveMaxLines : 0;
+
+ std::string content;
+ int lineCount = 0;
+ for (size_t i = startIdx; i < lines.size(); ++i)
+ {
+ if (!content.empty())
+ content += "\n";
+ content += lines[i];
+ lineCount++;
+ }
+
+ result.Content = winrt::to_hstring(content);
+ result.LineCount = lineCount;
+ result.Truncated = truncated;
+ }
+
+ co_return result;
+ }
+
+ IAsyncOperation TerminalPage::GetProtocolProcessStatus(winrt::guid sessionId)
+ {
+ auto strong = get_strong();
+
+ co_await wil::resume_foreground(Dispatcher());
+
+ Protocol::ProcessStatus result{};
+
+ for (const auto& tab : _tabs)
+ {
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ const auto rootPane = tabImpl->GetRootPane();
+ if (!rootPane)
+ continue;
+
+ const auto foundPane = _findPaneBySessionId(rootPane, sessionId);
+ if (!foundPane)
+ continue;
+
+ result.SessionId = sessionId;
+
+ const auto termControl = foundPane->GetTerminalControl();
+ if (!termControl)
+ {
+ result.State = L"unknown";
+ co_return result;
+ }
+
+ const auto conn = termControl.Connection();
+ if (!conn)
+ {
+ result.State = L"exited";
+ co_return result;
+ }
+
+ const auto connState = termControl.ConnectionState();
+
+ if (connState == ConnectionState::Connected)
+ {
+ result.State = L"running";
+ result.Pid = _getPidFromPane(foundPane);
+ }
+ else
+ {
+ result.State = L"exited";
+ if (const auto conpty = conn.try_as())
+ {
+ const auto handle = conpty.RootProcessHandle();
+ if (handle)
+ {
+ DWORD exitCode = 0;
+ if (GetExitCodeProcess(reinterpret_cast(handle), &exitCode))
+ {
+ if (exitCode != STILL_ACTIVE)
+ {
+ result.ExitCode = static_cast(exitCode);
+ result.HasExitCode = true;
+ }
+ }
+ result.Pid = static_cast(GetProcessId(reinterpret_cast(handle)));
+ }
+ }
+ }
+
+ co_return result;
+ }
+
+ co_return result; // empty SessionId = not found
+ }
+
+ IAsyncOperation TerminalPage::GetProtocolSessionVariable(winrt::guid /*sessionId*/, hstring /*name*/)
+ {
+ // Session variables require per-pane storage not yet ported.
+ co_return Protocol::SessionVariable{};
+ }
+
+ // ============================================================================
+ // Mutations — return typed structs or bool
+ // ============================================================================
+
+ IAsyncOperation TerminalPage::SetProtocolSessionVariable(winrt::guid /*sessionId*/, hstring /*name*/, hstring /*value*/)
+ {
+ // Session variables require per-pane storage not yet ported.
+ co_return false;
+ }
+
+ IAsyncOperation TerminalPage::CreateProtocolTab(NewTerminalArgs args, bool background)
+ {
+ auto strong = get_strong();
+ co_await wil::resume_foreground(Dispatcher());
+
+ Protocol::TabCreationResult result{};
+
+ auto pane = _MakePane(args, nullptr);
+ if (!pane)
+ co_return result;
+
+ _CreateNewTabFromPane(pane, static_cast(-1));
+ _tabContent.UpdateLayout(); // Force synchronous terminal initialization
+
+ if (_tabs.Size() == 0)
+ co_return result;
+
+ const auto newTabIdx = _tabs.Size() - 1;
+ const auto newTab = _tabs.GetAt(newTabIdx);
+ const auto tabImpl = _GetTabImpl(newTab);
+
+ result.TabId = newTabIdx;
+
+ if (tabImpl)
+ {
+ const auto rootPane = tabImpl->GetRootPane();
+ if (rootPane)
+ {
+ result.SessionId = _getSessionIdFromPane(rootPane);
+ result.Pid = _getPidFromPane(rootPane);
+ }
+ }
+
+ co_return result;
+ }
+
+ IAsyncOperation TerminalPage::SplitProtocolPane(winrt::guid sessionId, SplitDirection direction, float size, NewTerminalArgs args, bool background)
+ {
+ auto strong = get_strong();
+
+ co_await wil::resume_foreground(Dispatcher());
+
+ Protocol::TabCreationResult result{};
+
+ for (uint32_t tabIdx = 0; tabIdx < _tabs.Size(); ++tabIdx)
+ {
+ const auto tab = _tabs.GetAt(tabIdx);
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ const auto rootPane = tabImpl->GetRootPane();
+ if (!rootPane)
+ continue;
+
+ const auto foundPane = _findPaneBySessionId(rootPane, sessionId);
+ if (!foundPane)
+ continue;
+
+ if (const auto id = foundPane->Id())
+ {
+ tabImpl->FocusPane(id.value());
+ }
+
+ auto newPane = _MakePane(args, nullptr);
+ if (!newPane)
+ co_return result;
+
+ const auto newPanePid = _getPidFromPane(newPane);
+ auto newPaneRef = newPane; // copy shared_ptr before move
+
+ _SplitPane(tabImpl, direction, size, std::move(newPane));
+ _tabContent.UpdateLayout(); // Force synchronous terminal initialization
+
+ result.TabId = tabIdx;
+ result.SessionId = _getSessionIdFromPane(newPaneRef);
+ result.Pid = newPanePid;
+ co_return result;
+ }
+
+ co_return result;
+ }
+
+ IAsyncOperation TerminalPage::CloseProtocolPane(winrt::guid sessionId)
+ {
+ auto strong = get_strong();
+
+ co_await wil::resume_foreground(Dispatcher());
+
+ for (const auto& tab : _tabs)
+ {
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ const auto rootPane = tabImpl->GetRootPane();
+ if (!rootPane)
+ continue;
+
+ const auto foundPane = _findPaneBySessionId(rootPane, sessionId);
+ if (!foundPane)
+ continue;
+
+ foundPane->Close();
+ co_return true;
+ }
+
+ co_return false;
+ }
+
+ IAsyncOperation TerminalPage::SendProtocolInput(winrt::guid sessionId, hstring text)
+ {
+ auto strong = get_strong();
+ // Replace \n with \r — shells expect carriage return (Enter key)
+ // rather than line feed to execute commands.
+ std::wstring input{ text };
+ std::replace(input.begin(), input.end(), L'\n', L'\r');
+
+ co_await wil::resume_foreground(Dispatcher());
+
+ for (const auto& tab : _tabs)
+ {
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ const auto rootPane = tabImpl->GetRootPane();
+ if (!rootPane)
+ continue;
+
+ const auto foundPane = _findPaneBySessionId(rootPane, sessionId);
+ if (!foundPane)
+ continue;
+
+ const auto termControl = foundPane->GetTerminalControl();
+ if (!termControl)
+ co_return false;
+
+ termControl.SendInput(winrt::hstring{ input });
+ co_return true;
+ }
+
+ co_return false;
+ }
+
+ IAsyncOperation TerminalPage::FocusProtocolPane(winrt::guid sessionId)
+ {
+ auto strong = get_strong();
+
+ co_await wil::resume_foreground(Dispatcher());
+
+ for (const auto& tab : _tabs)
+ {
+ const auto tabImpl = _GetTabImpl(tab);
+ if (!tabImpl)
+ continue;
+
+ const auto rootPane = tabImpl->GetRootPane();
+ if (!rootPane)
+ continue;
+
+ const auto foundPane = _findPaneBySessionId(rootPane, sessionId);
+ if (!foundPane)
+ continue;
+
+ const auto paneId = foundPane->Id();
+ if (!paneId)
+ co_return false;
+
+ _SetFocusedTab(tab);
+
+ if (!tabImpl->FocusPane(paneId.value()))
+ co_return false;
+
+ if (const auto termControl = foundPane->GetTerminalControl())
+ {
+ termControl.Focus(winrt::Windows::UI::Xaml::FocusState::Programmatic);
+ }
+ co_return true;
+ }
+
+ co_return false;
+ }
+
+}
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 2705d4453c6..4e24b9bb276 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -229,9 +229,26 @@ namespace winrt::TerminalApp::implementation
uint32_t NumberOfTabs() const;
+ // Terminal Protocol Bridge Methods
+ uint32_t TabCount() const;
+ Windows::Foundation::IReference FocusedTabIndex() const;
+ Windows::Foundation::IAsyncOperation GetProtocolActivePane();
+ Windows::Foundation::IAsyncOperation> GetProtocolTabs();
+ Windows::Foundation::IAsyncOperation> GetProtocolPanes(uint32_t tabIdFilter);
+ Windows::Foundation::IAsyncOperation ReadProtocolPaneOutput(winrt::guid sessionId, hstring source, int32_t maxLines);
+ Windows::Foundation::IAsyncOperation GetProtocolProcessStatus(winrt::guid sessionId);
+ Windows::Foundation::IAsyncOperation GetProtocolSessionVariable(winrt::guid sessionId, hstring name);
+ Windows::Foundation::IAsyncOperation SetProtocolSessionVariable(winrt::guid sessionId, hstring name, hstring value);
+ Windows::Foundation::IAsyncOperation CreateProtocolTab(Microsoft::Terminal::Settings::Model::NewTerminalArgs args, bool background);
+ Windows::Foundation::IAsyncOperation SplitProtocolPane(winrt::guid sessionId, Microsoft::Terminal::Settings::Model::SplitDirection direction, float size, Microsoft::Terminal::Settings::Model::NewTerminalArgs args, bool background);
+ Windows::Foundation::IAsyncOperation CloseProtocolPane(winrt::guid sessionId);
+ Windows::Foundation::IAsyncOperation SendProtocolInput(winrt::guid sessionId, hstring text);
+ Windows::Foundation::IAsyncOperation FocusProtocolPane(winrt::guid sessionId);
+
til::property_changed_event PropertyChanged;
// -------------------------------- WinRT Events ---------------------------------
+ til::typed_event ProtocolVtSequenceReceived;
til::typed_event TitleChanged;
til::typed_event CloseWindowRequested;
til::typed_event SetTitleBarContent;
diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl
index 031ea0f2868..87c64103d22 100644
--- a/src/cascadia/TerminalApp/TerminalPage.idl
+++ b/src/cascadia/TerminalApp/TerminalPage.idl
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
import "TaskbarState.idl";
import "Remoting.idl";
+// Microsoft.Terminal.Protocol types are available via project reference (.winmd)
namespace TerminalApp
{
@@ -133,6 +134,23 @@ namespace TerminalApp
// one exists. No new wt.exe process is spawned.
event Windows.Foundation.TypedEventHandler
+
+ $(OpenConsoleCommonOutDir)TerminalProtocol\Microsoft.Terminal.Protocol.winmd
+ true
+ false
+ false
+
diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h
index ba0dcfda260..5cf58dcd87d 100644
--- a/src/cascadia/TerminalApp/pch.h
+++ b/src/cascadia/TerminalApp/pch.h
@@ -63,6 +63,7 @@
#include
#include
+#include
#include
#include
#include
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 3ca144bb4d8..229ae03030d 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -2387,6 +2387,40 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return hstring{ str };
}
+ hstring ControlCore::ReadLastPrompt() const
+ {
+ const auto lock = _terminal->LockForReading();
+ const auto& marks = _terminal->GetMarkExtents();
+ if (marks.empty())
+ {
+ return {};
+ }
+
+ const auto& textBuffer = _terminal->GetTextBuffer();
+
+ for (auto it = marks.rbegin(); it != marks.rend(); ++it)
+ {
+ if (!it->HasCommand())
+ {
+ continue;
+ }
+
+ // Skip until we find a finished command (saw FTCS CommandEnd).
+ if (!it->data.exitCode.has_value())
+ {
+ continue;
+ }
+
+ // Finished. Prefer [B..D]; if no output region was ever produced
+ // (e.g. `cd`), outputEnd is unset → fall back to [B..C] for the
+ // command text only.
+ const auto endPoint = it->outputEnd.value_or(*it->commandEnd);
+ return hstring{ textBuffer.GetPlainText(it->end, endPoint) };
+ }
+
+ return {};
+ }
+
// Get all of our recent commands. This will only really work if the user has enabled shell integration.
Control::CommandHistoryContext ControlCore::CommandHistory() const
{
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 1a03da9dbef..d9d1a1d0ff0 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -242,6 +242,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void SetReadOnlyMode(const bool readOnlyState);
hstring ReadEntireBuffer() const;
+ hstring ReadLastPrompt() const;
Control::CommandHistoryContext CommandHistory() const;
bool QuickFixesAvailable() const noexcept;
void UpdateQuickFixes(const Windows::Foundation::Collections::IVector& quickFixes);
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index d0ecdb7895a..440542a2064 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -171,6 +171,7 @@ namespace Microsoft.Terminal.Control
void EnablePainting();
String ReadEntireBuffer();
+ String ReadLastPrompt();
CommandHistoryContext CommandHistory();
Boolean QuickFixesAvailable { get; };
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index f549dc62584..a2eee6f8c94 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -3731,6 +3731,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return _core.ReadEntireBuffer();
}
+ hstring TermControl::ReadLastPrompt() const
+ {
+ return _core.ReadLastPrompt();
+ }
Control::CommandHistoryContext TermControl::CommandHistory() const
{
return _core.CommandHistory();
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 4011c76fc22..afa1851d233 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -172,6 +172,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
static Windows::UI::Xaml::Thickness ParseThicknessFromPadding(const hstring padding);
hstring ReadEntireBuffer() const;
+ hstring ReadLastPrompt() const;
Control::CommandHistoryContext CommandHistory() const;
void UpdateWinGetSuggestions(Windows::Foundation::Collections::IVector suggestions);
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index faf16be6c84..a7ff37e8a47 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -140,6 +140,7 @@ namespace Microsoft.Terminal.Control
void SetReadOnly(Boolean readOnlyState);
String ReadEntireBuffer();
+ String ReadLastPrompt();
CommandHistoryContext CommandHistory();
void UpdateWinGetSuggestions(Windows.Foundation.Collections.IVector suggestions);
diff --git a/src/cascadia/TerminalProtocol/ProtocolParsing.h b/src/cascadia/TerminalProtocol/ProtocolParsing.h
new file mode 100644
index 00000000000..6d6ee48b215
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/ProtocolParsing.h
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+// Extracted pure-parsing functions from the Terminal Protocol server layer
+// for fuzzing and testability. These functions have no COM, WinRT, or XAML
+// dependencies and can be called from a LibFuzzer harness.
+
+#pragma once
+
+#include
+
+namespace Microsoft::Terminal::Protocol::Parsing
+{
+ // ── SplitPane direction mapping ──
+
+ // Mirror of TerminalSettingsModel::SplitDirection enum values.
+ // Kept in sync with ActionArgs.idl.
+ enum class SplitDirection
+ {
+ Automatic = 0,
+ Up = 1,
+ Right = 2,
+ Down = 3,
+ Left = 4
+ };
+
+ // Map a direction string to a SplitDirection value.
+ // Accepts: "right", "left", "up", "down", "auto", "automatic",
+ // and legacy values "horizontal" (→ Down) / "vertical" (→ Right).
+ // Returns Right for unrecognized strings (matching server default).
+ inline SplitDirection ParseSplitDirection(const std::string& direction)
+ {
+ if (direction.empty())
+ {
+ return SplitDirection::Right;
+ }
+
+ if (direction == "right")
+ {
+ return SplitDirection::Right;
+ }
+ if (direction == "left")
+ {
+ return SplitDirection::Left;
+ }
+ if (direction == "up")
+ {
+ return SplitDirection::Up;
+ }
+ if (direction == "down")
+ {
+ return SplitDirection::Down;
+ }
+ if (direction == "auto" || direction == "automatic")
+ {
+ return SplitDirection::Automatic;
+ }
+ if (direction == "horizontal")
+ {
+ return SplitDirection::Down;
+ }
+ if (direction == "vertical")
+ {
+ return SplitDirection::Right;
+ }
+
+ // Unrecognized — default to Right
+ return SplitDirection::Right;
+ }
+
+ // ── ReadPaneOutput source routing ──
+
+ enum class PaneOutputSource
+ {
+ Scrollback,
+ Screen,
+ LastPrompt
+ };
+
+ // Classify the source parameter for ReadPaneOutput.
+ inline PaneOutputSource ClassifyPaneOutputSource(const std::string& source)
+ {
+ if (source == "last_prompt")
+ {
+ return PaneOutputSource::LastPrompt;
+ }
+ if (source == "screen")
+ {
+ return PaneOutputSource::Screen;
+ }
+ return PaneOutputSource::Scrollback;
+ }
+}
diff --git a/src/cascadia/TerminalProtocol/TerminalProtocol.idl b/src/cascadia/TerminalProtocol/TerminalProtocol.idl
new file mode 100644
index 00000000000..21eb29254b7
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/TerminalProtocol.idl
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+// Shared WinRT type definitions for the Terminal Protocol.
+
+namespace Microsoft.Terminal.Protocol
+{
+ struct WindowInfo
+ {
+ UInt64 WindowId;
+ String Title;
+ Boolean IsFocused;
+ UInt32 TabCount;
+ };
+
+ struct TabInfo
+ {
+ UInt32 TabId;
+ UInt64 WindowId;
+ String Title;
+ Boolean IsActive;
+ UInt32 PaneCount;
+ };
+
+ struct PaneInfo
+ {
+ Guid SessionId;
+ UInt32 TabId;
+ UInt64 WindowId;
+ String Title;
+ String Profile;
+ Boolean IsActive;
+ Boolean IsAgentPane;
+ UInt32 Pid;
+ Int32 Rows;
+ Int32 Columns;
+ // Current working directory of the pane's shell. Populated from the
+ // terminal control's working-directory tracking (OSC 9;9 / shell
+ // integration). Empty when not known.
+ String Cwd;
+ };
+
+ struct PaneOutput
+ {
+ Guid SessionId;
+ String Content;
+ Int32 LineCount;
+ Boolean Truncated;
+ // True when the content was sliced from an OSC 133 prompt mark
+ // (i.e., shell integration produced a parseable command boundary).
+ // False when no marks were available — the client should fall
+ // back to a line-count read of the same pane.
+ Boolean HasMarks;
+ };
+
+ struct ProcessStatus
+ {
+ Guid SessionId;
+ String State;
+ UInt32 Pid;
+ Int32 ExitCode;
+ Boolean HasExitCode;
+ };
+
+ struct SessionVariable
+ {
+ Guid SessionId;
+ String Name;
+ String Value;
+ Boolean Exists;
+ };
+
+ struct TabCreationResult
+ {
+ UInt32 TabId;
+ Guid SessionId;
+ UInt64 WindowId;
+ UInt32 Pid;
+ };
+}
+
diff --git a/src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj b/src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj
new file mode 100644
index 00000000000..8386ed407c0
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj
@@ -0,0 +1,38 @@
+
+
+
+ {F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5D}
+ Win32Proj
+ Microsoft.Terminal.Protocol
+ TerminalProtocol
+ TerminalProtocol
+ StaticLibrary
+ Console
+ true
+
+ true
+ true
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer-OneFuzzConfig.json b/src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer-OneFuzzConfig.json
new file mode 100644
index 00000000000..07628a8e95e
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer-OneFuzzConfig.json
@@ -0,0 +1,39 @@
+{
+ "configVersion": 3,
+ "entries": [
+ {
+ "Fuzzer": {
+ "$type": "libfuzzer",
+ "FuzzingHarnessExecutableName": "ProtocolFuzzer.exe",
+ "FuzzingTargetBinaries": [
+ "ProtocolFuzzer.exe"
+ ],
+ "FuzzingEntrypoint": "LLVMFuzzerTestOneInput"
+ },
+ "adoTemplate": {
+ "org": "microsoft",
+ "project": "Dart",
+ "AssignedTo": "yeelam@microsoft.com",
+ "AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
+ "IterationPath": "OS\\Future"
+ },
+ "jobNotificationEmail": "yeelam@microsoft.com",
+ "skip": false,
+ "rebootAfterSetup": false,
+ "oneFuzzJobs": [
+ {
+ "projectName": "IntelligentTerminal.TerminalProtocol",
+ "targetName": "Protocol_Fuzzer"
+ }
+ ],
+ "jobDependencies": [
+ "ProtocolFuzzer.exe",
+ "ProtocolFuzzer.pdb",
+ "ProtocolFuzzer.lib",
+ "clang_rt.asan_dynamic-x86_64.dll"
+ ],
+ "SdlWorkItemId": 62007365,
+ "SdlWorkItemProjectUrl": "https://dev.azure.com/microsoft/OS"
+ }
+ ]
+}
diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer.vcxproj b/src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer.vcxproj
new file mode 100644
index 00000000000..3367033d326
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer.vcxproj
@@ -0,0 +1,42 @@
+
+
+
+ {26711D2F-CA10-45BE-9E89-43771030BDB0}
+ Win32Proj
+ ProtocolFuzzer
+ Protocol.Fuzzer
+ ProtocolFuzzer
+ Application
+
+
+
+
+
+
+
+
+
+ Create
+ ProgramDatabase
+
+
+
+
+
+ ..;%(AdditionalIncludeDirectories)
+
+
+ Console
+
+
+
+
+
+ clang_rt.fuzzer_MT-$(OCClangArchitectureName).lib;%(AdditionalDependencies)
+
+
+
+
+
+
diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp
new file mode 100644
index 00000000000..e84b561e90c
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+// Fuzzing harness for Terminal Protocol server parsing logic.
+// Targets: ParseSplitDirection, ClassifyPaneOutputSource.
+//
+// Built under the Fuzzing MSBuild configuration with LibFuzzer
+// instrumentation; submittable to OneFuzz via the CI pipeline.
+
+#include "precomp.h"
+#include "ProtocolParsing.h"
+
+namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing;
+
+// Core fuzzing logic — called by both LibFuzzer and the manual main().
+static int FuzzOneInput(const uint8_t* data, size_t size)
+{
+ if (size == 0)
+ {
+ return 0;
+ }
+
+ const std::string input(reinterpret_cast(data), size);
+
+ // ── Target 1: ParseSplitDirection ──
+ // Feed fuzzed data as a direction string. Exercises all 8 recognized
+ // values plus arbitrary unrecognized strings.
+ {
+ ProtocolParsing::ParseSplitDirection(input);
+ }
+
+ // ── Target 2: ClassifyPaneOutputSource ──
+ // Feed fuzzed data as a source parameter.
+ {
+ ProtocolParsing::ClassifyPaneOutputSource(input);
+ }
+
+ return 0;
+}
+
+#ifdef FUZZING_BUILD
+extern "C" __declspec(dllexport) int LLVMFuzzerInitialize(int* /*argc*/, char*** /*argv*/)
+{
+ return 0;
+}
+#else
+int main(int argc, char** argv)
+{
+ if (argc < 2)
+ {
+ fprintf(stderr, "Usage: ProtocolFuzzer \n");
+ return 1;
+ }
+ std::ifstream file(argv[1], std::ios::binary);
+ std::string data((std::istreambuf_iterator(file)), std::istreambuf_iterator());
+ return FuzzOneInput(reinterpret_cast(data.data()), data.size());
+}
+#endif
+
+extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+ return FuzzOneInput(data, size);
+}
diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/precomp.cpp b/src/cascadia/TerminalProtocol/ft_fuzzer/precomp.cpp
new file mode 100644
index 00000000000..c51e9b31b2f
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/ft_fuzzer/precomp.cpp
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "precomp.h"
diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/precomp.h b/src/cascadia/TerminalProtocol/ft_fuzzer/precomp.h
new file mode 100644
index 00000000000..f06f78b03f8
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/ft_fuzzer/precomp.h
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#define NOMINMAX
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include
diff --git a/src/cascadia/TerminalProtocol/pch.cpp b/src/cascadia/TerminalProtocol/pch.cpp
new file mode 100644
index 00000000000..398a99f6653
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/pch.cpp
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
diff --git a/src/cascadia/TerminalProtocol/pch.h b/src/cascadia/TerminalProtocol/pch.h
new file mode 100644
index 00000000000..f3637a78952
--- /dev/null
+++ b/src/cascadia/TerminalProtocol/pch.h
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+#include
+#include
+#include
diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp
index 6d22a9a8182..ad6fd912bb2 100644
--- a/src/cascadia/WindowsTerminal/AppHost.cpp
+++ b/src/cascadia/WindowsTerminal/AppHost.cpp
@@ -10,6 +10,7 @@
#include "VirtualDesktopUtils.h"
#include "WindowEmperor.h"
+#include "TerminalProtocolComServer.h"
#include "../types/inc/utils.hpp"
using namespace winrt::Windows::UI;
diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp
new file mode 100644
index 00000000000..b28f1116049
--- /dev/null
+++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp
@@ -0,0 +1,692 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+
+#include "TerminalProtocolComServer.h"
+#include "WindowEmperor.h"
+#include "AppHost.h"
+
+#include
+#include
+#include "../TerminalProtocol/ProtocolParsing.h"
+
+#include
+#include
+#include
+
+#include
+#include
+
+using namespace Microsoft::WRL;
+
+namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing;
+
+namespace Protocol = winrt::Microsoft::Terminal::Protocol;
+
+// Static state — set once before registration, never mutated.
+WindowEmperor* TerminalProtocolComServer::s_emperor = nullptr;
+
+static DWORD g_comRegistration = 0;
+static std::shared_mutex g_mtx;
+static std::thread g_comMtaThread;
+static wil::unique_event g_comMtaStop;
+
+void TerminalProtocolComServer::s_setEmperor(WindowEmperor* emperor) noexcept
+{
+ s_emperor = emperor;
+}
+
+HRESULT TerminalProtocolComServer::s_StartListening()
+try
+{
+ std::unique_lock lock{ g_mtx };
+
+ // Register the COM class factory on a dedicated MTA thread so that
+ // incoming COM calls are dispatched to MTA worker threads rather than
+ // the STA/UI thread. This keeps long-running calls off the UI thread —
+ // and is what lets each method co_await onto the UI thread and .get()
+ // without blocking the UI thread.
+ g_comMtaStop.create(wil::EventOptions::ManualReset);
+
+ wil::unique_event ready(wil::EventOptions::ManualReset);
+ HRESULT regHr = S_OK;
+
+ g_comMtaThread = std::thread([&ready, ®Hr]() {
+ auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED);
+
+ // Classic-COM class factory (WRL) — marshaled via the OpenConsoleProxy
+ // proxy/stub, not WinRT MBM.
+ const auto factory = Make>();
+ if (!factory)
+ {
+ regHr = E_OUTOFMEMORY;
+ }
+ else
+ {
+ ComPtr unk;
+ regHr = factory.As(&unk);
+ if (SUCCEEDED(regHr))
+ {
+ regHr = CoRegisterClassObject(
+ __uuidof(TerminalProtocolComServer),
+ unk.Get(),
+ CLSCTX_LOCAL_SERVER,
+ REGCLS_MULTIPLEUSE,
+ &g_comRegistration);
+ }
+ }
+
+ ready.SetEvent();
+
+ // Keep this MTA thread alive so the COM registration stays active.
+ WaitForSingleObject(g_comMtaStop.get(), INFINITE);
+ });
+
+ ready.wait();
+ RETURN_IF_FAILED(regHr);
+ return S_OK;
+}
+CATCH_RETURN()
+
+HRESULT TerminalProtocolComServer::s_StopListening()
+{
+ std::unique_lock lock{ g_mtx };
+
+ if (g_comRegistration)
+ {
+ RETURN_IF_FAILED(CoRevokeClassObject(g_comRegistration));
+ g_comRegistration = 0;
+ }
+
+ // Signal the MTA thread to exit
+ if (g_comMtaStop)
+ {
+ g_comMtaStop.SetEvent();
+ }
+ if (g_comMtaThread.joinable())
+ {
+ g_comMtaThread.join();
+ }
+
+ return S_OK;
+}
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+static winrt::TerminalApp::TerminalPage _getPage(AppHost* host)
+{
+ if (!host)
+ return nullptr;
+ const auto logic = host->Logic();
+ if (!logic)
+ return nullptr;
+ const auto root = logic.GetRoot();
+ if (!root)
+ return nullptr;
+ return root.try_as();
+}
+
+// Parse a JSON string into Json::Value
+static bool _parseJson(const std::string& str, Json::Value& out)
+{
+ Json::CharReaderBuilder rb;
+ std::string errs;
+ std::istringstream ss(str);
+ return Json::parseFromStream(rb, ss, &out, &errs);
+}
+
+// ── JSON serialization helpers (the wire format is JSON; see wtcli Formatting) ──
+
+static std::string _guidStr(const winrt::guid& g)
+{
+ wchar_t buf[40]{};
+ ::StringFromGUID2(g, buf, ARRAYSIZE(buf));
+ std::wstring ws{ buf };
+ if (ws.size() > 2 && ws.front() == L'{' && ws.back() == L'}')
+ ws = ws.substr(1, ws.size() - 2);
+ return winrt::to_string(winrt::hstring{ ws });
+}
+
+// Allocate a BSTR from a UTF-8 std::string. Throws E_OUTOFMEMORY on allocation
+// failure so the (CATCH_RETURN-wrapped) caller returns a failure HRESULT rather
+// than S_OK with a null out-param.
+static BSTR _bstr(const std::string& utf8)
+{
+ BSTR b = ::SysAllocString(winrt::to_hstring(utf8).c_str());
+ THROW_IF_NULL_ALLOC(b);
+ return b;
+}
+
+// Serialize a Json::Value to a compact BSTR.
+static BSTR _bstrFromJson(const Json::Value& v)
+{
+ Json::StreamWriterBuilder wb;
+ wb["indentation"] = "";
+ return _bstr(Json::writeString(wb, v));
+}
+
+static Json::Value _toJson(const Protocol::WindowInfo& w)
+{
+ Json::Value v;
+ v["window_id"] = static_cast(w.WindowId);
+ v["title"] = winrt::to_string(w.Title);
+ v["is_focused"] = static_cast(w.IsFocused);
+ v["tab_count"] = static_cast(w.TabCount);
+ return v;
+}
+
+static Json::Value _toJson(const Protocol::TabInfo& t)
+{
+ Json::Value v;
+ v["tab_id"] = static_cast(t.TabId);
+ v["window_id"] = static_cast(t.WindowId);
+ v["title"] = winrt::to_string(t.Title);
+ v["is_active"] = static_cast(t.IsActive);
+ v["pane_count"] = static_cast(t.PaneCount);
+ return v;
+}
+
+static Json::Value _toJson(const Protocol::PaneInfo& p)
+{
+ Json::Value v;
+ v["session_id"] = _guidStr(p.SessionId);
+ v["tab_id"] = static_cast(p.TabId);
+ v["window_id"] = static_cast(p.WindowId);
+ v["title"] = winrt::to_string(p.Title);
+ v["profile"] = winrt::to_string(p.Profile);
+ v["is_active"] = static_cast(p.IsActive);
+ v["is_agent_pane"] = static_cast(p.IsAgentPane);
+ v["pid"] = static_cast(p.Pid);
+ v["size"]["rows"] = p.Rows;
+ v["size"]["columns"] = p.Columns;
+ v["cwd"] = winrt::to_string(p.Cwd);
+ return v;
+}
+
+static Json::Value _toJson(const Protocol::PaneOutput& o)
+{
+ Json::Value v;
+ v["session_id"] = _guidStr(o.SessionId);
+ v["content"] = winrt::to_string(o.Content);
+ v["line_count"] = o.LineCount;
+ v["truncated"] = static_cast(o.Truncated);
+ v["has_marks"] = static_cast(o.HasMarks);
+ return v;
+}
+
+static Json::Value _toJson(const Protocol::ProcessStatus& s)
+{
+ Json::Value v;
+ v["session_id"] = _guidStr(s.SessionId);
+ v["state"] = winrt::to_string(s.State);
+ v["pid"] = static_cast(s.Pid);
+ v["has_exit_code"] = static_cast(s.HasExitCode);
+ // Only emit exit_code when it's meaningful; clients gate on has_exit_code,
+ // so omitting it for a still-running process avoids a misleading 0.
+ if (s.HasExitCode)
+ v["exit_code"] = s.ExitCode;
+ return v;
+}
+
+static Json::Value _toJson(const Protocol::SessionVariable& s)
+{
+ Json::Value v;
+ v["session_id"] = _guidStr(s.SessionId);
+ v["name"] = winrt::to_string(s.Name);
+ v["value"] = winrt::to_string(s.Value);
+ v["exists"] = static_cast(s.Exists);
+ return v;
+}
+
+static Json::Value _toJson(const Protocol::TabCreationResult& r)
+{
+ Json::Value v;
+ v["tab_id"] = static_cast(r.TabId);
+ v["session_id"] = _guidStr(r.SessionId);
+ v["window_id"] = static_cast(r.WindowId);
+ v["pid"] = static_cast(r.Pid);
+ return v;
+}
+
+// Convert an [in] BSTR to a winrt::hstring (null-safe).
+static winrt::hstring _hstr(BSTR b)
+{
+ return b ? winrt::hstring{ b } : winrt::hstring{};
+}
+
+// ============================================================================
+// Queries
+// ============================================================================
+
+STDMETHODIMP TerminalProtocolComServer::GetActivePane(BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ const auto host = s_emperor->GetMostRecentWindow();
+ RETURN_HR_IF(E_FAIL, !host);
+
+ const auto page = _getPage(host);
+ RETURN_HR_IF(E_FAIL, !page);
+
+ auto info = page.GetProtocolActivePane().get();
+ RETURN_HR_IF(E_FAIL, info.SessionId == winrt::guid{});
+
+ // TerminalPage doesn't know the window ID — fill it in here.
+ const auto& props = host->Logic().WindowProperties();
+ info.WindowId = props.WindowId();
+
+ *json = _bstrFromJson(_toJson(info));
+ return S_OK;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::ListWindows(BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ const auto mostRecent = s_emperor->GetMostRecentWindow();
+ Json::Value arr(Json::arrayValue);
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto logic = host->Logic();
+ if (!logic)
+ continue;
+
+ const auto& props = logic.WindowProperties();
+
+ Protocol::WindowInfo info{};
+ info.WindowId = props.WindowId();
+ info.Title = props.WindowNameForDisplay();
+ info.IsFocused = (host.get() == mostRecent);
+
+ const auto page = _getPage(host.get());
+ info.TabCount = page ? page.TabCount() : 0;
+ arr.append(_toJson(info));
+ }
+
+ *json = _bstrFromJson(arr);
+ return S_OK;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::ListTabs(unsigned __int64 windowIdFilter, BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ Json::Value arr(Json::arrayValue);
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto logic = host->Logic();
+ if (!logic)
+ continue;
+
+ const auto& props = logic.WindowProperties();
+ if (windowIdFilter != 0 && props.WindowId() != windowIdFilter)
+ continue;
+
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ const auto windowId = props.WindowId();
+ const auto tabs = page.GetProtocolTabs().get();
+ for (uint32_t i = 0; i < tabs.Size(); ++i)
+ {
+ auto t = tabs.GetAt(i);
+ t.WindowId = windowId;
+ arr.append(_toJson(t));
+ }
+ }
+
+ *json = _bstrFromJson(arr);
+ return S_OK;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::ListPanes(unsigned __int64 windowIdFilter, unsigned long tabIdFilter, BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ Json::Value arr(Json::arrayValue);
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto logic = host->Logic();
+ if (!logic)
+ continue;
+
+ const auto& props = logic.WindowProperties();
+ if (windowIdFilter != 0 && props.WindowId() != windowIdFilter)
+ continue;
+
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ const auto windowId = props.WindowId();
+ const auto panes = page.GetProtocolPanes(tabIdFilter).get();
+ for (uint32_t i = 0; i < panes.Size(); ++i)
+ {
+ auto p = panes.GetAt(i);
+ p.WindowId = windowId;
+ arr.append(_toJson(p));
+ }
+ }
+
+ *json = _bstrFromJson(arr);
+ return S_OK;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::ReadPaneOutput(GUID sessionId, BSTR source, long maxLines, BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ const auto src = _hstr(source);
+ const auto effectiveSource = src.empty() ? winrt::hstring{ L"scrollback" } : src;
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ auto info = page.ReadProtocolPaneOutput(winrt::guid{ sessionId }, effectiveSource, maxLines).get();
+ if (info.SessionId != winrt::guid{})
+ {
+ *json = _bstrFromJson(_toJson(info));
+ return S_OK;
+ }
+ }
+
+ return E_FAIL; // Pane not found
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::GetProcessStatus(GUID sessionId, BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ auto info = page.GetProtocolProcessStatus(winrt::guid{ sessionId }).get();
+ if (info.SessionId != winrt::guid{})
+ {
+ *json = _bstrFromJson(_toJson(info));
+ return S_OK;
+ }
+ }
+
+ return E_FAIL;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::GetSessionVariable(GUID sessionId, BSTR name, BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ auto info = page.GetProtocolSessionVariable(winrt::guid{ sessionId }, _hstr(name)).get();
+ if (info.SessionId != winrt::guid{})
+ {
+ *json = _bstrFromJson(_toJson(info));
+ return S_OK;
+ }
+ }
+
+ return E_FAIL;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::GetSettings(BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+
+ const std::filesystem::path settingsPath{
+ std::wstring_view{ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings::SettingsPath() }
+ };
+ *json = _bstr(til::io::read_file_as_utf8_string_if_exists(settingsPath));
+ return S_OK;
+}
+CATCH_RETURN()
+
+// ============================================================================
+// Mutations
+// ============================================================================
+
+STDMETHODIMP TerminalProtocolComServer::CreateTab(unsigned __int64 windowId,
+ BSTR profile,
+ BSTR commandline,
+ BSTR title,
+ BSTR startingDirectory,
+ boolean suppressAppTitle,
+ boolean background,
+ BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+
+ // Find target window.
+ AppHost* targetHost = nullptr;
+ if (windowId != 0)
+ {
+ targetHost = s_emperor->GetWindowById(windowId);
+ }
+ else
+ {
+ targetHost = s_emperor->GetMostRecentWindow();
+ }
+ RETURN_HR_IF(E_FAIL, !targetHost);
+
+ const auto page = _getPage(targetHost);
+ RETURN_HR_IF(E_FAIL, !page);
+
+ // Build NewTerminalArgs.
+ winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTermArgs;
+ const auto profileH = _hstr(profile);
+ const auto commandlineH = _hstr(commandline);
+ const auto titleH = _hstr(title);
+ const auto startingDirectoryH = _hstr(startingDirectory);
+ if (!profileH.empty())
+ newTermArgs.Profile(profileH);
+ if (!commandlineH.empty())
+ newTermArgs.Commandline(commandlineH);
+ if (!startingDirectoryH.empty())
+ newTermArgs.StartingDirectory(startingDirectoryH);
+ if (!titleH.empty())
+ {
+ newTermArgs.TabTitle(titleH);
+ if (suppressAppTitle)
+ newTermArgs.SuppressApplicationTitle(true);
+ }
+
+ auto cr = page.CreateProtocolTab(newTermArgs, background != 0).get();
+ RETURN_HR_IF(E_FAIL, cr.SessionId == winrt::guid{});
+
+ const auto& props = targetHost->Logic().WindowProperties();
+ cr.WindowId = props.WindowId();
+ *json = _bstrFromJson(_toJson(cr));
+ return S_OK;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::SplitPane(GUID sessionId,
+ BSTR direction,
+ float size,
+ BSTR profile,
+ BSTR commandline,
+ boolean background,
+ BSTR* json)
+try
+{
+ RETURN_HR_IF_NULL(E_POINTER, json);
+ *json = nullptr;
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+ RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{});
+
+ // Map direction string to SplitDirection enum via shared parsing logic.
+ const auto parsedDir = ProtocolParsing::ParseSplitDirection(winrt::to_string(_hstr(direction)));
+ auto splitDir = static_cast(
+ static_cast(parsedDir));
+
+ // Build NewTerminalArgs.
+ winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTermArgs;
+ const auto profileH = _hstr(profile);
+ const auto commandlineH = _hstr(commandline);
+ if (!profileH.empty())
+ newTermArgs.Profile(profileH);
+ if (!commandlineH.empty())
+ newTermArgs.Commandline(commandlineH);
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ auto cr = page.SplitProtocolPane(winrt::guid{ sessionId }, splitDir, size, newTermArgs, background != 0).get();
+ if (cr.SessionId == winrt::guid{})
+ continue; // pane not in this window
+
+ const auto& props = host->Logic().WindowProperties();
+ cr.WindowId = props.WindowId();
+ *json = _bstrFromJson(_toJson(cr));
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::ClosePane(GUID sessionId)
+try
+{
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+ RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{});
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ if (page.CloseProtocolPane(winrt::guid{ sessionId }).get())
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::SendInput(GUID sessionId, BSTR text)
+try
+{
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+ RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{});
+
+ const auto textH = _hstr(text);
+
+ // Empty input is a no-op, matching ControlCore::SendInput semantics so
+ // COM clients that send "" don't see surprising E_INVALIDARG failures.
+ if (textH.empty())
+ {
+ return S_OK;
+ }
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ if (page.SendProtocolInput(winrt::guid{ sessionId }, textH).get())
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::FocusPane(GUID sessionId)
+try
+{
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+ RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{});
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ if (page.FocusProtocolPane(winrt::guid{ sessionId }).get())
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+CATCH_RETURN()
+
+STDMETHODIMP TerminalProtocolComServer::SetSessionVariable(GUID sessionId, BSTR name, BSTR value)
+try
+{
+ RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor);
+ RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{});
+
+ const auto nameH = _hstr(name);
+ RETURN_HR_IF(E_INVALIDARG, nameH.empty());
+
+ for (const auto& host : s_emperor->GetWindows())
+ {
+ const auto page = _getPage(host.get());
+ if (!page)
+ continue;
+
+ if (page.SetProtocolSessionVariable(winrt::guid{ sessionId }, nameH, _hstr(value)).get())
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+CATCH_RETURN()
+
+
diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h
new file mode 100644
index 00000000000..1610755f003
--- /dev/null
+++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include
+#include
+
+#include "ITerminalProtocol.h"
+
+// Per-brand CLSIDs — same pattern as CTerminalHandoff.
+#if defined(WT_BRANDING_RELEASE)
+#define __CLSID_TerminalProtocolServer "832FDEC7-AA6F-4BAB-85FA-A491405638FC"
+#elif defined(WT_BRANDING_PREVIEW)
+#define __CLSID_TerminalProtocolServer "D77C8A1A-83C0-42FC-BADF-9BE82E2A1624"
+#elif defined(WT_BRANDING_CANARY)
+#define __CLSID_TerminalProtocolServer "264DE65B-F597-4183-8A76-6A039A604725"
+#else
+#define __CLSID_TerminalProtocolServer "AD9425AA-1722-4E7B-A451-AA1D09106E83"
+#endif
+
+class WindowEmperor;
+
+// Classic-COM server for ITerminalProtocol. Marshaled by the OpenConsoleProxy
+// proxy/stub (NOT WinRT Metadata-Based Marshaling), so activation/marshaling
+// never goes through the combase WinRT activation catalog
+// (CWinRTActivationStoreCatalog) implicated in the 0xc0000005 / 0x80010105
+// failures. Complex results cross the wire as JSON (BSTR)
+struct __declspec(uuid(__CLSID_TerminalProtocolServer))
+TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags,
+ ITerminalProtocol>
+{
+ // ── ITerminalProtocol ──
+ STDMETHODIMP GetActivePane(BSTR* json) override;
+ STDMETHODIMP ListWindows(BSTR* json) override;
+ STDMETHODIMP ListTabs(unsigned __int64 windowIdFilter, BSTR* json) override;
+ STDMETHODIMP ListPanes(unsigned __int64 windowIdFilter, unsigned long tabIdFilter, BSTR* json) override;
+ STDMETHODIMP ReadPaneOutput(GUID sessionId, BSTR source, long maxLines, BSTR* json) override;
+ STDMETHODIMP GetProcessStatus(GUID sessionId, BSTR* json) override;
+ STDMETHODIMP GetSessionVariable(GUID sessionId, BSTR name, BSTR* json) override;
+ STDMETHODIMP GetSettings(BSTR* json) override;
+ STDMETHODIMP CreateTab(unsigned __int64 windowId, BSTR profile, BSTR commandline, BSTR title, BSTR startingDirectory, boolean suppressAppTitle, boolean background, BSTR* json) override;
+ STDMETHODIMP SplitPane(GUID sessionId, BSTR direction, float size, BSTR profile, BSTR commandline, boolean background, BSTR* json) override;
+ STDMETHODIMP ClosePane(GUID sessionId) override;
+ STDMETHODIMP SendInput(GUID sessionId, BSTR text) override;
+ STDMETHODIMP FocusPane(GUID sessionId) override;
+ STDMETHODIMP SetSessionVariable(GUID sessionId, BSTR name, BSTR value) override;
+
+ // Static setup — must be called before s_StartListening().
+ static void s_setEmperor(WindowEmperor* emperor) noexcept;
+
+ static HRESULT s_StartListening();
+ static HRESULT s_StopListening();
+
+private:
+
+ static WindowEmperor* s_emperor;
+};
diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
index ab9e0003899..2232d8067bc 100644
--- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp
+++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
@@ -15,11 +15,16 @@
#include
#include "AppHost.h"
+#include "TerminalProtocolComServer.h"
#include "resource.h"
#include "VirtualDesktopUtils.h"
#include "../../types/inc/User32Utils.hpp"
#include "../../types/inc/utils.hpp"
+#include
+#include
+#pragma comment(lib, "bcrypt.lib")
+
using namespace winrt;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings::Model;
@@ -28,6 +33,13 @@ using namespace ::Microsoft::Console;
using namespace std::chrono_literals;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
+WindowEmperor::WindowEmperor() = default;
+WindowEmperor::~WindowEmperor()
+{
+ // Revoke COM class factory before destroying resources.
+ LOG_IF_FAILED(TerminalProtocolComServer::s_StopListening());
+}
+
#ifdef _WIN64
static constexpr ULONG_PTR TERMINAL_HANDOFF_MAGIC = 0x4c414e494d524554; // 'TERMINAL'
#else
@@ -561,6 +573,9 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
_checkWindowsForNotificationIcon();
_setupSessionPersistence(_app.Logic().Settings().GlobalSettings().ShouldUsePersistedLayout());
+ // Initialize the protocol server for AI CLI integration.
+ _initializeProtocolServer();
+
// When the settings change, we'll want to update our global hotkeys
// and our notification icon based on the new settings.
_app.Logic().SettingsChanged([this](auto&&, const TerminalApp::SettingsLoadEventArgs& args) {
@@ -1716,3 +1731,15 @@ void WindowEmperor::_checkWindowsForNotificationIcon()
}
#pragma endregion
+
+// ============================================================================
+// Protocol Server
+// ============================================================================
+
+void WindowEmperor::_initializeProtocolServer()
+{
+ // Register COM class factory for cross-process access (runs on MTA thread).
+ TerminalProtocolComServer::s_setEmperor(this);
+ SUCCEEDED_LOG(TerminalProtocolComServer::s_StartListening());
+ OutputDebugStringA("WT Protocol Server started\n");
+}
\ No newline at end of file
diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h
index f34c07919db..2ea6245dd0b 100644
--- a/src/cascadia/WindowsTerminal/WindowEmperor.h
+++ b/src/cascadia/WindowsTerminal/WindowEmperor.h
@@ -18,9 +18,11 @@ Class Name:
#pragma once
class AppHost;
+struct TerminalProtocolComServer;
class WindowEmperor
{
+
public:
enum UserMessages : UINT
{
@@ -40,16 +42,22 @@ class WindowEmperor
std::wstring Name;
};
+ WindowEmperor();
+ ~WindowEmperor();
+
HWND GetMainWindow() const noexcept;
AppHost* GetWindowById(uint64_t id) const noexcept;
AppHost* GetWindowByName(std::wstring_view name) const noexcept;
- // CreateNewWindow is used for creating a new window from existing Content
void CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args);
void HandleCommandlineArgs(int nCmdShow);
void FocusTabInAnyWindow(const winrt::TerminalApp::Tab& tab) const;
// OpenWindow is used for opening a new window or summoning an existing window by name.
void OpenWindow(const winrt::hstring& name);
+ // Protocol server access
+ const std::vector>& GetWindows() const noexcept { return _windows; }
+ AppHost* GetMostRecentWindow() const noexcept { return _mostRecentWindow(); }
+
private:
struct SummonWindowSelectionArgs
{
@@ -88,6 +96,9 @@ class WindowEmperor
wil::unique_hwnd _window;
winrt::TerminalApp::App _app{ nullptr };
std::vector> _windows;
+
+ // Protocol server for AI CLI integration
+ void _initializeProtocolServer();
std::vector _hotkeys;
NOTIFYICONDATA _notificationIcon{};
UINT WM_TASKBARCREATED = 0;
diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj
index 5135d643fb0..47df7aeea2b 100644
--- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj
+++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj
@@ -36,10 +36,10 @@
- $(OpenConsoleDir)\src\inc;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\Console;$(OpenConsoleDir)\dep\Win32K;$(OpenConsoleDir)\dep\gsl\include;%(AdditionalIncludeDirectories);
+ $(IntDir)..\OpenConsoleProxy;$(OpenConsoleDir)\src\inc;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\Console;$(OpenConsoleDir)\dep\Win32K;$(OpenConsoleDir)\dep\gsl\include;%(AdditionalIncludeDirectories);
- gdi32.lib;dwmapi.lib;Shcore.lib;UxTheme.lib;%(AdditionalDependencies)
+ gdi32.lib;dwmapi.lib;Shcore.lib;UxTheme.lib;ole32.lib;%(AdditionalDependencies)
@@ -56,6 +56,7 @@
+
@@ -67,6 +68,7 @@
+
@@ -93,11 +95,9 @@
-
+
$(OpenConsoleCommonOutDir)TerminalCore\Microsoft.Terminal.Core.winmd
@@ -105,6 +105,12 @@
false
false
+
+ $(OpenConsoleCommonOutDir)TerminalProtocol\Microsoft.Terminal.Protocol.winmd
+ true
+ false
+ false
+
+ $(IntDir)..\OpenConsoleProxy;%(AdditionalIncludeDirectories)
+
+
+ Console
+ ole32.lib;WindowsApp.lib;%(AdditionalDependencies)
+
+
+
+
+
+ {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}
+ false
+
+
+
+
+