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 RequestOpenWindow; + // Terminal Protocol Bridge Methods + UInt32 TabCount { get; }; + Windows.Foundation.IReference FocusedTabIndex { get; }; + Windows.Foundation.IAsyncOperation GetProtocolActivePane(); + Windows.Foundation.IAsyncOperation > GetProtocolTabs(); + Windows.Foundation.IAsyncOperation > GetProtocolPanes(UInt32 tabIdFilter); + Windows.Foundation.IAsyncOperation ReadProtocolPaneOutput(Guid sessionId, String source, Int32 maxLines); + Windows.Foundation.IAsyncOperation GetProtocolProcessStatus(Guid sessionId); + Windows.Foundation.IAsyncOperation GetProtocolSessionVariable(Guid sessionId, String name); + Windows.Foundation.IAsyncOperation SetProtocolSessionVariable(Guid sessionId, String name, String value); + Windows.Foundation.IAsyncOperation CreateProtocolTab(Microsoft.Terminal.Settings.Model.NewTerminalArgs args, Boolean background); + Windows.Foundation.IAsyncOperation SplitProtocolPane(Guid sessionId, Microsoft.Terminal.Settings.Model.SplitDirection direction, Single size, Microsoft.Terminal.Settings.Model.NewTerminalArgs args, Boolean background); + Windows.Foundation.IAsyncOperation CloseProtocolPane(Guid sessionId); + Windows.Foundation.IAsyncOperation SendProtocolInput(Guid sessionId, String text); + Windows.Foundation.IAsyncOperation FocusProtocolPane(Guid sessionId); + event Windows.Foundation.TypedEventHandler ProtocolVtSequenceReceived; + event Windows.Foundation.TypedEventHandler RequestNewWindow; } } diff --git a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj index 69ccb921f83..76fc71e8601 100644 --- a/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj +++ b/src/cascadia/TerminalApp/dll/TerminalApp.vcxproj @@ -94,6 +94,12 @@ false false + + $(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 + + + + +