From f43921c58afe1be42ff95026abe391a5d374a5f2 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 17 Jun 2026 10:22:33 -0700 Subject: [PATCH 01/26] some build errors --- OpenConsole.slnx | 21 + doc/wtcli-commands.md | 54 + .../CascadiaPackage/CascadiaPackage.wapproj | 3 + .../TerminalApp/TerminalAppLib.vcxproj | 10 + .../TerminalApp/TerminalPage.Protocol.cpp | 649 +++++++++++ src/cascadia/TerminalApp/TerminalPage.h | 17 + src/cascadia/TerminalApp/TerminalPage.idl | 18 + src/cascadia/TerminalApp/pch.h | 1 + .../TerminalProtocol/ProtocolParsing.h | 198 ++++ .../TerminalProtocol/TerminalProtocol.idl | 129 +++ .../TerminalProtocol/TerminalProtocol.vcxproj | 38 + .../ProtocolFuzzer-OneFuzzConfig.json | 39 + .../ft_fuzzer/ProtocolFuzzer.vcxproj | 42 + .../TerminalProtocol/ft_fuzzer/fuzzmain.cpp | 72 ++ .../TerminalProtocol/ft_fuzzer/precomp.cpp | 4 + .../TerminalProtocol/ft_fuzzer/precomp.h | 15 + src/cascadia/TerminalProtocol/pch.cpp | 4 + src/cascadia/TerminalProtocol/pch.h | 7 + src/cascadia/WindowsTerminal/AppHost.cpp | 5 + .../TerminalProtocolComServer.cpp | 1009 +++++++++++++++++ .../TerminalProtocolComServer.h | 105 ++ .../WindowsTerminal/WindowEmperor.cpp | 45 + src/cascadia/WindowsTerminal/WindowEmperor.h | 15 +- .../WindowsTerminal/WindowsTerminal.vcxproj | 20 +- src/cascadia/WindowsTerminal/pch.h | 1 + src/host/proxy/Host.Proxy.vcxproj | 8 + src/host/proxy/ITerminalProtocol.idl | 79 ++ src/tools/wtcli/Formatting.cpp | 96 ++ src/tools/wtcli/Formatting.h | 19 + .../ft_fuzzer/WtcliFuzzer-OneFuzzConfig.json | 39 + src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj | 56 + src/tools/wtcli/ft_fuzzer/fuzzmain.cpp | 111 ++ src/tools/wtcli/ft_fuzzer/precomp.cpp | 4 + src/tools/wtcli/ft_fuzzer/precomp.h | 17 + src/tools/wtcli/main.cpp | 885 +++++++++++++++ src/tools/wtcli/wtcli.vcxproj | 50 + src/tools/wtcli/wtcli_functions.h | 215 ++++ 37 files changed, 4092 insertions(+), 8 deletions(-) create mode 100644 doc/wtcli-commands.md create mode 100644 src/cascadia/TerminalApp/TerminalPage.Protocol.cpp create mode 100644 src/cascadia/TerminalProtocol/ProtocolParsing.h create mode 100644 src/cascadia/TerminalProtocol/TerminalProtocol.idl create mode 100644 src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj create mode 100644 src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer-OneFuzzConfig.json create mode 100644 src/cascadia/TerminalProtocol/ft_fuzzer/ProtocolFuzzer.vcxproj create mode 100644 src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp create mode 100644 src/cascadia/TerminalProtocol/ft_fuzzer/precomp.cpp create mode 100644 src/cascadia/TerminalProtocol/ft_fuzzer/precomp.h create mode 100644 src/cascadia/TerminalProtocol/pch.cpp create mode 100644 src/cascadia/TerminalProtocol/pch.h create mode 100644 src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp create mode 100644 src/cascadia/WindowsTerminal/TerminalProtocolComServer.h create mode 100644 src/host/proxy/ITerminalProtocol.idl create mode 100644 src/tools/wtcli/Formatting.cpp create mode 100644 src/tools/wtcli/Formatting.h create mode 100644 src/tools/wtcli/ft_fuzzer/WtcliFuzzer-OneFuzzConfig.json create mode 100644 src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj create mode 100644 src/tools/wtcli/ft_fuzzer/fuzzmain.cpp create mode 100644 src/tools/wtcli/ft_fuzzer/precomp.cpp create mode 100644 src/tools/wtcli/ft_fuzzer/precomp.h create mode 100644 src/tools/wtcli/main.cpp create mode 100644 src/tools/wtcli/wtcli.vcxproj create mode 100644 src/tools/wtcli/wtcli_functions.h diff --git a/OpenConsole.slnx b/OpenConsole.slnx index 9dff529afa7..20a200c7259 100644 --- a/OpenConsole.slnx +++ b/OpenConsole.slnx @@ -449,6 +449,7 @@ + @@ -1046,4 +1047,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md new file mode 100644 index 00000000000..1dfc6314920 --- /dev/null +++ b/doc/wtcli-commands.md @@ -0,0 +1,54 @@ +# wtcli Command Reference + +`wtcli` is the CLI client for the Windows Terminal Protocol. It looks up the +running Terminal via the `WT_COM_CLSID` environment variable, calls +`CoCreateInstance(CLSCTX_LOCAL_SERVER)` to obtain `IProtocolServer`, and +exposes a tmux-style command surface over its IDL methods. + +- Source: `src/tools/wtcli/main.cpp` +- IDL: `src/cascadia/TerminalProtocol/TerminalProtocol.idl` +- Primary in-tree caller: `tools/wta/src/shell/wt_channel/cli_channel.rs` (and + `tools/wta/src/app.rs` for `publish`). + +## 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`) | +| `wait-for` | — | Block (poll `pane-status`) until the pane process exits. `--interval` is poll period in ms; `--timeout` is seconds (`0` = forever). | `wtcli wait-for -t 3 --timeout 60` | ❌ Not called. (`wta` exposes its own `wait-for` subcommand at `tools/wta/src/main.rs:209`, but its handler polls by shelling out to `wtcli pane-status` in a Rust loop — it does **not** invoke `wtcli wait-for`.) | +| `listen` | — | Long-running. Subscribe to `IProtocolServer` and stream every event JSON line to stdout until Ctrl-C. `-t` filters by pane id; `--event` filters by type and supports a trailing `*` wildcard. | `wtcli --json listen --event "agent.*"` | ✅ `cli_channel.rs` (background listener task) | +| `send-event` | `se` | Publish an event using the `agent_event` envelope: sets `type=event`, `method=agent_event`, fills `params.event` from `-e` and `params.pane_id` from `-p` (or the active pane). Extra params come from the trailing JSON object. | `wtcli send-event -p 3 -e agent.task.completed '{"exit_code":0}'` | ❌ Not called from in-tree code. Documented as the public CLI surface for external agents in `doc/specs/llm-agent-event-integration.md`. | +| `publish` | — | Low-level escape hatch: forwards a raw JSON string straight to `IProtocolServer::SendEvent` with no envelope. Used for events that don't fit the `agent_event` shape (e.g. `autofix_state` routed directly to `TerminalPage`). | `wtcli publish '{"method":"autofix_state","params":{"state":"ready"}}'` | ✅ `tools/wta/src/app.rs` (`publish_event_blocking`) | +| `info` | — | Print `WT_COM_CLSID`, connection status, protocol version, and the server's `GetCapabilities()` method list. | `wtcli --json info` | ✅ `cli_channel.rs` maps `get_capabilities` → `wtcli info` | +| `test-pipe` | — | Smoke test: connect, run `list-windows` + `get_capabilities`, print results. Diagnostic only. | `wtcli test-pipe` | ❌ Not called. Manual diagnostic. | +| `set-env` | `setenv` | Print shell-specific export statements for `WT_COM_CLSID` (`-s powershell\|bash\|cmd`). Output is meant to be `eval`'d / `Invoke-Expression`'d by the caller; it does not modify the current process. | `wtcli set-env -s powershell \| Invoke-Expression` | ❌ Not called. Manual recovery for child shells that didn't inherit `WT_COM_CLSID`. | + +## Summary + +- **Wired into `wta` runtime (13):** `list-windows`, `list-tabs`, + `list-panes`, `active-pane`, `capture-pane`, `pane-status`, + `new-tab`, `split-pane`, `kill-pane`, `focus-pane`, + `listen`, `info`, `publish`. +- **Defined but not invoked from in-tree code (4):** `wait-for`, + `send-event`, `test-pipe`, `set-env`. These remain as public surface for + external agents / shell scripts and for manual debugging. 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/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index 371dbd1746e..f3c6499536e 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 @@ -467,6 +471,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..2ffdfe6bb81 --- /dev/null +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -0,0 +1,649 @@ +// 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. They return typed WinRT structs across +// the DLL boundary. +// +// 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, -1, /*openInBackground=*/background); + _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), /*focusNewPane=*/!background); + _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 74aade5f191..31ad1ed82f1 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -187,9 +187,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 27be726db0f..9457e9c8dc3 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 { @@ -104,6 +105,23 @@ namespace TerminalApp event Windows.Foundation.TypedEventHandler RequestLaunchPosition; + // 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/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/TerminalProtocol/ProtocolParsing.h b/src/cascadia/TerminalProtocol/ProtocolParsing.h new file mode 100644 index 00000000000..ae5f307d00d --- /dev/null +++ b/src/cascadia/TerminalProtocol/ProtocolParsing.h @@ -0,0 +1,198 @@ +// 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 +#include + +#include + +namespace Microsoft::Terminal::Protocol::Parsing +{ + // ── JSON helper ── + + // Parse a JSON string. Returns true on success. + // Equivalent to _parseJson() in TerminalProtocolComServer.cpp. + inline 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); + } + + // ── SendEvent dispatch ── + + // The dispatch routes for IProtocolServer::SendEvent. + enum class SendEventRoute + { + AutofixState, // Direct to TerminalPage, no broadcast + AgentStatus, // Direct to TerminalPage, no broadcast + CloseAgentPane, // Direct to TerminalPage, no broadcast + AgentState, // Direct to TerminalPage, no broadcast — unified per-tab agent-pane UI snapshot (view + pane_open + ...) + ResumeInNewAgentTab, // Direct to TerminalPage, no broadcast + AgentChipTarget, // Direct to TerminalPage, no broadcast — "draw the Agent chip on this pane (or hide override)" + RestartAgentStack, // Direct to TerminalPage, no broadcast — `/restart` from any agent pane TUI + RestartAgentPane, // Direct to TerminalPage, no broadcast — master detected helper death; re-warm a fresh helper for this tab + Broadcast, // Normalize envelope + broadcast to all subscribers + Invalid // Failed validation + }; + + // Classify and validate a SendEvent JSON payload. + // + // On success, |outEvt| contains the parsed JSON. For the Broadcast route + // the envelope is normalized (type=event, method=agent_event). + // + // Returns Invalid when: + // - JSON parsing fails + // - The broadcast path is selected but params.event is missing + inline SendEventRoute ClassifySendEvent(const std::string& eventJson, Json::Value& outEvt) + { + if (!ParseJson(eventJson, outEvt)) + { + return SendEventRoute::Invalid; + } + + // Event JSON must be an object to inspect fields. + if (!outEvt.isObject()) + { + return SendEventRoute::Invalid; + } + + // Check method-based direct dispatch routes + if (outEvt.isMember("method") && outEvt["method"].isString()) + { + const auto method = outEvt["method"].asString(); + if (method == "autofix_state") + { + return SendEventRoute::AutofixState; + } + if (method == "agent_status") + { + return SendEventRoute::AgentStatus; + } + if (method == "close_agent_pane") + { + return SendEventRoute::CloseAgentPane; + } + if (method == "agent_state_changed") + { + return SendEventRoute::AgentState; + } + if (method == "resume_in_new_agent_tab") + { + return SendEventRoute::ResumeInNewAgentTab; + } + if (method == "set_agent_chip_target") + { + return SendEventRoute::AgentChipTarget; + } + if (method == "restart_agent_stack") + { + return SendEventRoute::RestartAgentStack; + } + if (method == "restart_agent_pane") + { + return SendEventRoute::RestartAgentPane; + } + } + + // Broadcast path: params.event is required + if (!outEvt.isMember("params") || !outEvt["params"].isObject() || + !outEvt["params"].isMember("event")) + { + return SendEventRoute::Invalid; + } + + // Normalize the envelope + outEvt["type"] = "event"; + outEvt["method"] = "agent_event"; + + return SendEventRoute::Broadcast; + } + + // ── 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..95f9a09ef1c --- /dev/null +++ b/src/cascadia/TerminalProtocol/TerminalProtocol.idl @@ -0,0 +1,129 @@ +// 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; + }; + + struct AuthResult + { + Boolean Authenticated; + String ProtocolVersion; + }; + + // Callback interface — implemented by clients (e.g. wtcli) to receive + // push-based event notifications from Terminal. + interface IProtocolEventCallback + { + void OnEvent(String eventJson); + } + + // Server interface — implemented by WindowsTerminal.exe, consumed by wtcli.exe. + // Activated via CoCreateInstance (CLSCTX_LOCAL_SERVER) with a custom class factory. + // Marshaled cross-process via MBM (no proxy/stub DLL needed). + interface IProtocolServer + { + // Meta + AuthResult Authenticate(String token); + String GetCapabilities(); + + // Queries + PaneInfo GetActivePane(); + WindowInfo[] ListWindows(); + TabInfo[] ListTabs(UInt64 windowIdFilter); + PaneInfo[] ListPanes(UInt64 windowIdFilter, UInt32 tabIdFilter); + PaneOutput ReadPaneOutput(Guid sessionId, String source, Int32 maxLines); + ProcessStatus GetProcessStatus(Guid sessionId); + SessionVariable GetSessionVariable(Guid sessionId, String name); + String GetSettings(); + + // Mutations + TabCreationResult CreateTab(UInt64 windowId, String profile, String commandline, String title, String startingDirectory, Boolean suppressAppTitle, Boolean background); + TabCreationResult SplitPane(Guid sessionId, String direction, Single size, String profile, String commandline, Boolean background); + void ClosePane(Guid sessionId); + void SendInput(Guid sessionId, String text); + void FocusPane(Guid sessionId); + void SetSessionVariable(Guid sessionId, String name, String value); + + // Events — push-based via callback + void Subscribe(IProtocolEventCallback callback); + void Unsubscribe(); + + // Client-originated event publishing (agent → WT → listeners) + void SendEvent(String eventJson); + } +} + 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..a2619c51b11 --- /dev/null +++ b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Fuzzing harness for Terminal Protocol server parsing logic. +// Targets: ClassifySendEvent, 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: ClassifySendEvent ── + // Feed fuzzed data as a SendEvent JSON payload. Exercises JSON parsing, + // method field inspection, and params.event validation across all 3 + // dispatch routes (AutofixState, AgentStatus, Broadcast). + { + Json::Value evt; + ProtocolParsing::ClassifySendEvent(input, evt); + } + + // ── Target 2: ParseSplitDirection ── + // Feed fuzzed data as a direction string. Exercises all 8 recognized + // values plus arbitrary unrecognized strings. + { + ProtocolParsing::ParseSplitDirection(input); + } + + // ── Target 3: 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 1b59ba3571d..66f0ac1ea36 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; @@ -1193,6 +1194,10 @@ safe_void_coroutine AppHost::_WindowInitializedHandler(const winrt::Windows::Fou { _isWindowInitialized = WindowInitializedState::Initializing; + // Re-run page-events registration now that the TerminalPage is + // actually constructed. Ensures this window's events reach COM clients. + TerminalProtocolComServer::s_OnWindowAdded(this); + // GH#11561: We're totally done being initialized. Resize the window to // match the initial settings, and then call ShowWindow to finally make us // visible. diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp new file mode 100644 index 00000000000..74920db137e --- /dev/null +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -0,0 +1,1009 @@ +// 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; + +// Static instance tracking for event delivery to COM clients +std::mutex TerminalProtocolComServer::s_instancesMutex; +std::vector TerminalProtocolComServer::s_instances; + +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; +} + +TerminalProtocolComServer::~TerminalProtocolComServer() +{ + _removeInstance(); +} + +void TerminalProtocolComServer::_addInstance() +{ + std::lock_guard lock{ s_instancesMutex }; + if (_instanceRegistered) + return; + _instanceRegistered = true; + s_instances.push_back(this); +} + +void TerminalProtocolComServer::_removeInstance() +{ + std::lock_guard lock{ s_instancesMutex }; + std::erase(s_instances, this); +} + +// ============================================================================ +// 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{}; +} + +void TerminalProtocolComServer::_ensurePageEventsRegistered() +{ + if (!s_emperor) + return; + + // Per-TerminalPage registration tracking. Each window gets its + // ProtocolVtSequenceReceived wired exactly once, so events from any + // window — not just the lexically-first one — reach the COM fan-out. + // + // Key on `winrt::weak_ref` rather than `AppHost*`: + // - A closed window's AppHost is destructed and the same memory + // address may later be reused by a freshly-created AppHost. With a + // raw-pointer key the new window would be misidentified as + // "already registered" and silently skipped (ABA bug). + // - `weak_ref` tracks the WinRT object's actual identity; dead + // entries surface as `weak_ref::get()` returning null, and new + // pages are recognized as distinct even if they share an address + // with a defunct one. + static std::mutex s_regMutex; + static std::vector> s_registered; + + std::lock_guard lock{ s_regMutex }; + + // Prune dead entries (page destructed → weak_ref returns null). + std::erase_if(s_registered, [](const auto& w) { return !w.get(); }); + + // Register any TerminalPage we haven't seen before. If the page + // isn't ready yet (early startup race), skip silently — the next + // Subscribe() or s_OnWindowAdded() call will retry. + for (const auto& host : s_emperor->GetWindows()) + { + const auto page = _getPage(host.get()); + if (!page) + { + continue; + } + + const bool alreadyRegistered = std::any_of( + s_registered.begin(), + s_registered.end(), + [&page](const auto& w) { + const auto p = w.get(); + return p && p == page; + }); + if (alreadyRegistered) + { + continue; + } + + page.ProtocolVtSequenceReceived( + [](auto&&, const winrt::hstring& eventJson) { + s_NotifyEventToComClients(winrt::to_string(eventJson)); + }); + s_registered.push_back(winrt::make_weak(page)); + } +} + +void TerminalProtocolComServer::s_OnWindowAdded(AppHost* /*host*/) +{ + _ensurePageEventsRegistered(); +} + +void TerminalProtocolComServer::s_NotifyEventToComClients(const std::string& eventJson) +{ + wil::unique_bstr eventBstr{ ::SysAllocString(winrt::to_hstring(eventJson).c_str()) }; + if (!eventBstr) + { + // OOM allocating the event payload — skip this delivery rather than + // calling OnEvent with a null BSTR, which would break the documented + // JSON-string event contract. + return; + } + + // Snapshot the agile sink references under lock, then invoke outside the + // lock to avoid deadlocks if a callback reenters the server (e.g. via + // SendEvent). + std::vector> sinks; + { + std::lock_guard lock{ s_instancesMutex }; + for (auto* instance : s_instances) + { + std::lock_guard cbLock{ instance->_callbackMutex }; + if (instance->_sinkRef) + sinks.push_back(instance->_sinkRef); + } + } + + // TODO: event fan-out runs on the UI/STA thread (ProtocolVtSequenceReceived + // is raised there), and each OnEvent below is a SYNCHRONOUS cross-process + // call. A slow or blocked subscriber (e.g. wtcli's stdout pipe full because + // wta isn't draining it) therefore stalls the terminal UI thread, and the + // cost is serial across all subscribers. For high event volume this should + // be moved off the UI thread — queue events and drain them on a dedicated + // background MTA thread (mind ordering, back-pressure, and subscriber + // add/remove thread-safety), and/or give OnEvent a timeout. Tracked as + // follow-up; not addressed in the WinRT->classic-COM migration. + for (auto& ref : sinks) + { + // Resolve the agile reference to a sink proxy valid on THIS thread, + // then call it — this is the cross-apartment-safe callback path. + ComPtr sink; + if (FAILED(ref->Resolve(IID_PPV_ARGS(&sink))) || !sink) + continue; + + if (FAILED(sink->OnEvent(eventBstr.get()))) + { + // Client disconnected — clear the matching sink reference. + std::lock_guard lock{ s_instancesMutex }; + for (auto* instance : s_instances) + { + std::lock_guard cbLock{ instance->_callbackMutex }; + if (instance->_sinkRef.Get() == ref.Get()) + { + instance->_sinkRef.Reset(); + break; + } + } + } + } +} + +// ============================================================================ +// ITerminalProtocol — Meta +// ============================================================================ + +STDMETHODIMP TerminalProtocolComServer::Authenticate(BSTR /*token*/, BSTR* resultJson) +try +{ + RETURN_HR_IF_NULL(E_POINTER, resultJson); + *resultJson = nullptr; + RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); + + // DEV BYPASS: always authenticate — credential plumbing not yet implemented. + _authenticated = true; + + // Register for event delivery on successful authentication. + _addInstance(); + + Json::Value v; + v["authenticated"] = _authenticated; + // 2.2 — SendInput restored on the COM surface; pane identifiers remain GUIDs. + v["protocol_version"] = "2.2"; + *resultJson = _bstrFromJson(v); + return S_OK; +} +CATCH_RETURN() + +STDMETHODIMP TerminalProtocolComServer::GetCapabilities(BSTR* json) +try +{ + RETURN_HR_IF_NULL(E_POINTER, json); + *json = nullptr; + + static const std::vector supportedMethods = { + "authenticate", + "get_capabilities", + "get_active_pane", + "list_windows", + "list_tabs", + "list_panes", + "read_pane_output", + "get_process_status", + "get_session_variable", + "get_settings", + "create_tab", + "split_pane", + "close_pane", + "send_input", + "focus_pane", + "set_session_variable", + "subscribe", + "unsubscribe", + "send_event", + }; + + Json::Value methods(Json::arrayValue); + for (const auto& m : supportedMethods) + methods.append(m); + + *json = _bstrFromJson(methods); + return S_OK; +} +CATCH_RETURN() + +// ============================================================================ +// 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); + info.TabCount = logic.TabCount(); + 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() + +// ============================================================================ +// Events — push-based via classic sink +// ============================================================================ + +STDMETHODIMP TerminalProtocolComServer::Subscribe(ITerminalProtocolEventSink* sink) +try +{ + RETURN_HR_IF(E_INVALIDARG, !sink); + RETURN_HR_IF(E_ACCESSDENIED, !_authenticated); + + // Store the sink as an agile reference so it can be resolved + called from + // any apartment (the VT-event fan-out runs on the UI/STA thread, while the + // sink was unmarshaled on an MTA thread). + ComPtr ref; + RETURN_IF_FAILED(::RoGetAgileReference(AGILEREFERENCE_DEFAULT, __uuidof(ITerminalProtocolEventSink), sink, &ref)); + + { + std::lock_guard lock{ _callbackMutex }; + _sinkRef = ref; + } + + // Ensure page events are wired up (one-time global init). + _ensurePageEventsRegistered(); + return S_OK; +} +CATCH_RETURN() + +STDMETHODIMP TerminalProtocolComServer::Unsubscribe() +{ + std::lock_guard lock{ _callbackMutex }; + _sinkRef.Reset(); + return S_OK; +} + +STDMETHODIMP TerminalProtocolComServer::SendEvent(BSTR eventJson) +try +{ + RETURN_HR_IF(E_ACCESSDENIED, !_authenticated); + + const auto eventH = _hstr(eventJson); + auto jsonStr = winrt::to_string(eventH); + Json::Value evt; + const auto route = ProtocolParsing::ClassifySendEvent(jsonStr, evt); + RETURN_HR_IF(E_INVALIDARG, route == ProtocolParsing::SendEventRoute::Invalid); + + switch (route) + { + case ProtocolParsing::SendEventRoute::AutofixState: + _dispatchAutofixStateToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::AgentStatus: + _dispatchAgentStatusToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::CloseAgentPane: + // User pressed Ctrl+C twice in the wta TUI. Marshal to the UI + // thread; the page-side handler resolves the tab via `tab_id` + // and tears down that tab's agent pane. + _dispatchCloseAgentPaneToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::AgentState: + // Per-tab agent-pane UI snapshot from wta. Page-side handler + // routes by `tab_id` to the matching AgentPaneContent (creating + // or tearing down the pane on that tab as needed). + _dispatchAgentStateChangedToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::ResumeInNewAgentTab: + // Session view's Shift+Enter handler in the wta TUI. WT creates + // a new tab and asks wta to open an agent pane in it. + _dispatchResumeInNewAgentTabToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::AgentChipTarget: + // Helper override for which pane gets the "Agent" chip; null + // pane_session_id reverts the tab to source-flag-driven chip. + _dispatchAgentChipTargetToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::RestartAgentStack: + // `/restart` from any agent pane TUI. Page-side handler tears + // down every agent pane in its window and force-restarts the + // wta-master process via SharedWta. + _dispatchRestartAgentStackToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::RestartAgentPane: + // Master detected a helper's pipe disconnect (crash or clean + // exit). Page-side handler resolves the tab via `tab_id` and + // re-warms a fresh helper, resuming `session_id`. Suppressed when + // the pane was torn down deliberately (Ctrl+C×2, tab close). + _dispatchRestartAgentPaneToPage(eventH); + return S_OK; + case ProtocolParsing::SendEventRoute::Broadcast: + { + Json::StreamWriterBuilder wb; + wb["indentation"] = ""; + s_NotifyEventToComClients(Json::writeString(wb, evt)); + return S_OK; + } + default: + return S_OK; + } +} +CATCH_RETURN() + +// Agent-pane dispatch methods — stubbed out (agent pane infrastructure not ported). +void TerminalProtocolComServer::_dispatchAutofixStateToPage(const winrt::hstring& /*eventJson*/) {} +void TerminalProtocolComServer::_dispatchAgentStatusToPage(const winrt::hstring& /*eventJson*/) {} +void TerminalProtocolComServer::_dispatchCloseAgentPaneToPage(const winrt::hstring& /*eventJson*/) {} +void TerminalProtocolComServer::_dispatchAgentStateChangedToPage(const winrt::hstring& /*eventJson*/) {} +void TerminalProtocolComServer::_dispatchResumeInNewAgentTabToPage(const winrt::hstring& /*eventJson*/) {} +void TerminalProtocolComServer::_dispatchAgentChipTargetToPage(const winrt::hstring& /*eventJson*/) {} +void TerminalProtocolComServer::_dispatchRestartAgentStackToPage(const winrt::hstring& /*eventJson*/) {} +void TerminalProtocolComServer::_dispatchRestartAgentPaneToPage(const winrt::hstring& /*eventJson*/) {} + + diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h new file mode 100644 index 00000000000..350dc84489c --- /dev/null +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include + +#include +#include + +#include "ITerminalProtocol.h" + +// Per-brand CLSIDs — same pattern as CTerminalHandoff. Reused unchanged from the +// previous WinRT/MBM server, so WT_COM_CLSID discovery on the client is identical. +#if defined(WT_BRANDING_RELEASE) +#define __CLSID_TerminalProtocolServer "A2E4F6B8-1C3D-4E5F-A6B7-C8D9E0F1A2B3" +#elif defined(WT_BRANDING_PREVIEW) +#define __CLSID_TerminalProtocolServer "B3F5A7C9-2D4E-4F6A-B7C8-D9E0F1A2B3C4" +#elif defined(WT_BRANDING_CANARY) +#define __CLSID_TerminalProtocolServer "C4A6B8D0-3E5F-4A7B-C8D9-E0F1A2B3C4D5" +#else +#define __CLSID_TerminalProtocolServer "D5B7C9E1-4F6A-4B8C-D9E0-F1A2B3C4D5E6" +#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); the per-method logic +// and the UI-thread-marshaled page queries are unchanged from the WinRT server. +struct __declspec(uuid(__CLSID_TerminalProtocolServer)) +TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + ITerminalProtocol> +{ + ~TerminalProtocolComServer(); + + // ── ITerminalProtocol ── + STDMETHODIMP Authenticate(BSTR token, BSTR* resultJson) override; + STDMETHODIMP GetCapabilities(BSTR* json) override; + 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; + STDMETHODIMP Subscribe(ITerminalProtocolEventSink* sink) override; + STDMETHODIMP Unsubscribe() override; + STDMETHODIMP SendEvent(BSTR eventJson) override; + + // Static setup — must be called before s_StartListening(). + static void s_setEmperor(WindowEmperor* emperor) noexcept; + + static HRESULT s_StartListening(); + static HRESULT s_StopListening(); + + // Re-runs per-window page event registration after a new AppHost is added. + static void s_OnWindowAdded(class AppHost* host); + + // Deliver an event to all connected COM clients. + static void s_NotifyEventToComClients(const std::string& eventJson); + +private: + bool _authenticated = false; + + // Per-instance event sink, stored as an agile reference so it can be + // resolved + called from any apartment (set via Subscribe, cleared via + // Unsubscribe). + std::mutex _callbackMutex; + Microsoft::WRL::ComPtr _sinkRef; + + // Static tracking of live COM instances for event delivery. + static std::mutex s_instancesMutex; + static std::vector s_instances; + + bool _instanceRegistered{ false }; + + void _addInstance(); + void _removeInstance(); + static void _ensurePageEventsRegistered(); + + // Per-method UI-thread dispatch helpers (unchanged from the WinRT server; + // they marshal SendEvent payloads onto each window's TerminalPage). + static void _dispatchAutofixStateToPage(const winrt::hstring& eventJson); + static void _dispatchAgentStatusToPage(const winrt::hstring& eventJson); + static void _dispatchCloseAgentPaneToPage(const winrt::hstring& eventJson); + static void _dispatchAgentStateChangedToPage(const winrt::hstring& eventJson); + static void _dispatchResumeInNewAgentTabToPage(const winrt::hstring& eventJson); + static void _dispatchAgentChipTargetToPage(const winrt::hstring& eventJson); + static void _dispatchRestartAgentStackToPage(const winrt::hstring& eventJson); + static void _dispatchRestartAgentPaneToPage(const winrt::hstring& eventJson); + + static WindowEmperor* s_emperor; +}; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 9fa13c1b1c0..406c14ffb76 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 @@ -272,6 +284,11 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args _windowCount += 1; _windows.emplace_back(std::move(host)); + // Wire the new window's TerminalPage::ProtocolVtSequenceReceived + // into the COM fan-out so events emitted by panes in this window + // actually reach connected wta clients. + TerminalProtocolComServer::s_OnWindowAdded(_windows.back().get()); + if (_windowCount == 1) { // The first CoreWindow is created implicitly by XAML and parented to the @@ -498,6 +515,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) { @@ -1626,3 +1646,28 @@ 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); + if (SUCCEEDED_LOG(TerminalProtocolComServer::s_StartListening())) + { + // Stringify the CLSID so child processes can discover us via CoCreateInstance. + wil::unique_cotaskmem_string clsidStr; + if (SUCCEEDED(StringFromCLSID(__uuidof(TerminalProtocolComServer), &clsidStr)) + && clsidStr) + { + _comClsid = clsidStr.get(); + SetEnvironmentVariableW(L"WT_COM_CLSID", _comClsid.c_str()); + } + } + + OutputDebugStringA(fmt::format("WT Protocol Server started\n WT_COM_CLSID={}\n", + winrt::to_string(_comClsid)) + .c_str()); +} \ No newline at end of file diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 5eb93d64012..8bef6f272b3 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 { @@ -30,14 +32,21 @@ class WindowEmperor WM_NOTIFY_FROM_NOTIFICATION_AREA, }; + 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; + // Protocol server access + const std::wstring& GetComClsid() const noexcept { return _comClsid; } + const std::vector>& GetWindows() const noexcept { return _windows; } + AppHost* GetMostRecentWindow() const noexcept { return _mostRecentWindow(); } + private: struct SummonWindowSelectionArgs { @@ -75,6 +84,10 @@ class WindowEmperor wil::unique_hwnd _window; winrt::TerminalApp::App _app{ nullptr }; std::vector> _windows; + + // Protocol server for AI CLI integration + std::wstring _comClsid; // Stringified CLSID for WT_COM_CLSID env var + 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 + + + _DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION;%(PreprocessorDefinitions) + + + clang_rt.fuzzer_MT-$(OCClangArchitectureName).lib;%(AdditionalDependencies) + + + xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)" + Copy the required ASan runtime DLL to the output directory. + + + + + + + + + diff --git a/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp b/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp new file mode 100644 index 00000000000..63b47f751e0 --- /dev/null +++ b/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Fuzzing harness for wtcli CLI utility functions. +// Targets: BuildSendEventJson, MatchesEventFilter. +// +// Built under the Fuzzing MSBuild configuration with LibFuzzer +// instrumentation; submittable to OneFuzz via the CI pipeline. + +#include "precomp.h" +#include "wtcli_functions.h" + +// Split fuzz input into N segments using null bytes as delimiters. +// If fewer than N null bytes exist, remaining segments are empty. +static std::vector SplitInput(const uint8_t* data, size_t size, size_t n) +{ + std::vector segments(n); + size_t seg = 0; + size_t start = 0; + for (size_t i = 0; i < size && seg < n - 1; ++i) + { + if (data[i] == '\0') + { + segments[seg].assign(reinterpret_cast(data + start), i - start); + start = i + 1; + ++seg; + } + } + // Remainder goes into the last segment. + if (start < size) + { + segments[seg].assign(reinterpret_cast(data + start), size - start); + } + return segments; +} + +// 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; + } + + // Split the entire input into segments for use across all targets. + // We need at least 4 segments: [eventType] [paramsJson] + // [sessionId] [eventTypeFilter] + auto parts = SplitInput(data, size, 4); + + // ── Target 1: BuildSendEventJson ── + // Fuzz all three input parameters: eventType, paramsJson, and sessionId. + { + Json::Value evt; + wtcli::BuildSendEventJson(parts[0], parts[1], parts[2], evt); + } + + // ── Target 2: MatchesEventFilter ── + // Construct valid JSON from fuzzed fields using Json::Value so the parser + // succeeds and the deep matching logic (session_id, wildcard) is reached + // even when fuzzed strings contain quotes/backslashes/control chars. + { + // 2a: Fuzzed event structure with fuzzed filter. + const auto& fuzzedSessionId = parts[2]; + const auto& fuzzedEvent = parts[0]; + const auto& fuzzedFilter = parts[3]; + + Json::Value params; + params["session_id"] = fuzzedSessionId; + params["event"] = fuzzedEvent; + Json::Value ev; + ev["params"] = params; + Json::StreamWriterBuilder wb; + wb["indentation"] = ""; + auto syntheticJson = Json::writeString(wb, ev); + + wtcli::MatchesEventFilter(syntheticJson, fuzzedSessionId, fuzzedFilter); + + // 2b: Mismatched session_id — exercises the rejection path. + wtcli::MatchesEventFilter(syntheticJson, "999", fuzzedFilter); + + // 2c: Raw fuzzed bytes as event JSON — exercises parse-failure path. + const std::string raw(reinterpret_cast(data), size); + wtcli::MatchesEventFilter(raw, fuzzedSessionId, fuzzedFilter); + } + + 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: WtcliFuzzer \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/tools/wtcli/ft_fuzzer/precomp.cpp b/src/tools/wtcli/ft_fuzzer/precomp.cpp new file mode 100644 index 00000000000..c51e9b31b2f --- /dev/null +++ b/src/tools/wtcli/ft_fuzzer/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/tools/wtcli/ft_fuzzer/precomp.h b/src/tools/wtcli/ft_fuzzer/precomp.h new file mode 100644 index 00000000000..768ce96863e --- /dev/null +++ b/src/tools/wtcli/ft_fuzzer/precomp.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#define NOMINMAX + +#include + +#include +#include +#include +#include +#include +#include + +#include diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp new file mode 100644 index 00000000000..a8e4e3b8ecc --- /dev/null +++ b/src/tools/wtcli/main.cpp @@ -0,0 +1,885 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#include "Formatting.h" +#include "wtcli_functions.h" + +// Classic-COM Terminal protocol. Generated from +// src/host/proxy/ITerminalProtocol.idl; found via the OpenConsoleProxy IntDir +// added to this project's include path. Marshaled by the OpenConsoleProxy +// proxy/stub (NOT WinRT MBM), so activation/marshaling never hits the combase +// WinRT activation catalog. +#include "ITerminalProtocol.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ── EventSink — pure classic-COM event sink for `listen` ── +struct EventSink : ITerminalProtocolEventSink +{ + LONG _ref{ 1 }; + std::function _handler; + + explicit EventSink(std::function handler) : + _handler(std::move(handler)) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override + { + if (!ppv) + return E_POINTER; + if (riid == __uuidof(IUnknown) || riid == __uuidof(ITerminalProtocolEventSink)) + { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + *ppv = nullptr; + return E_NOINTERFACE; + } + ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&_ref); } + ULONG STDMETHODCALLTYPE Release() override + { + const auto r = InterlockedDecrement(&_ref); + if (r == 0) + delete this; + return r; + } + HRESULT STDMETHODCALLTYPE OnEvent(BSTR eventJson) override + { + if (_handler) + _handler(eventJson ? winrt::to_string(winrt::hstring{ eventJson }) : std::string{}); + return S_OK; + } +}; + +// ── Helpers ── + +static winrt::com_ptr ConnectToTerminal(bool* outAuthenticated = nullptr, std::string* outVersion = nullptr) +{ + wchar_t clsid[128]{}; + if (!GetEnvironmentVariableW(L"WT_COM_CLSID", clsid, ARRAYSIZE(clsid))) + { + fprintf(stderr, "[wtcli] WT_COM_CLSID not set. Must run inside a Windows Terminal pane.\n"); + return nullptr; + } + + CLSID cls{}; + if (FAILED(CLSIDFromString(clsid, &cls))) + { + fprintf(stderr, "[wtcli] Invalid CLSID: %ls\n", clsid); + return nullptr; + } + + winrt::com_ptr server; + auto hr = CoCreateInstance(cls, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(ITerminalProtocol), server.put_void()); + if (FAILED(hr)) + { + fprintf(stderr, "[wtcli] Connection failed: 0x%08X\n", static_cast(hr)); + return nullptr; + } + + BSTR rawAuth = nullptr; + hr = server->Authenticate(nullptr, &rawAuth); + bool parsed = false; + bool authenticated = false; + std::string version; + if (SUCCEEDED(hr) && rawAuth) + { + Json::Value v; + Json::CharReaderBuilder rb; + std::string errs; + auto s = winrt::to_string(winrt::hstring{ rawAuth }); + std::istringstream ss(s); + if (Json::parseFromStream(rb, ss, &v, &errs)) + { + parsed = true; + authenticated = v["authenticated"].asBool(); + version = v["protocol_version"].asString(); + } + } + if (rawAuth) + SysFreeString(rawAuth); + + if (FAILED(hr)) + { + fprintf(stderr, "[wtcli] Authentication failed: 0x%08X\n", static_cast(hr)); + return nullptr; + } + if (!parsed) + { + // Success HRESULT but a null/malformed auth payload is a broken + // server contract — don't misreport it as a server rejection. + fprintf(stderr, "[wtcli] Authentication response missing or malformed (server contract error)\n"); + return nullptr; + } + if (!authenticated) + { + fprintf(stderr, "[wtcli] Authentication rejected by server\n"); + return nullptr; + } + + if (outAuthenticated) + *outAuthenticated = authenticated; + if (outVersion) + *outVersion = version; + return server; +} + +// Call a method that returns a JSON BSTR; parse into `out`. Returns the HRESULT. +template +static HRESULT CallJson(F&& call, Json::Value& out) +{ + BSTR raw = nullptr; + HRESULT hr = call(&raw); + if (SUCCEEDED(hr)) + { + bool parsed = false; + if (raw) + { + Json::CharReaderBuilder rb; + std::string errs; + auto s = winrt::to_string(winrt::hstring{ raw }); + std::istringstream ss(s); + parsed = Json::parseFromStream(rb, ss, &out, &errs); + } + // A success HRESULT with a null BSTR or malformed JSON is a broken + // server contract; surface it as an error so callers' FAILED(hr) + // checks fire immediately instead of proceeding with a + // default-constructed `out`. + if (!parsed) + hr = E_UNEXPECTED; + } + if (raw) + SysFreeString(raw); + return hr; +} + +static std::string GuidToString(const 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 }); +} + +static GUID GuidFromString(const std::string& target) +{ + auto wstr = winrt::to_hstring(target); + std::wstring guidStr{ wstr }; + if (!guidStr.empty() && guidStr[0] != L'{') + guidStr = L"{" + guidStr + L"}"; + GUID g{}; + if (FAILED(CLSIDFromString(guidStr.c_str(), &g))) + { + if (!target.empty()) + fprintf(stderr, "[wtcli] Invalid session ID: %s\n", target.c_str()); + return GUID{}; + } + return g; +} + +// Resolve a session id: an explicit GUID string, or the active pane's id. +static GUID ResolveSessionId(ITerminalProtocol* server, const std::string& target) +{ + if (!target.empty()) + return GuidFromString(target); + + Json::Value info; + const auto hr = CallJson([&](BSTR* j) { return server->GetActivePane(j); }, info); + if (FAILED(hr)) + { + fprintf(stderr, "[wtcli] Could not resolve active pane (GetActivePane failed: 0x%08X)\n", static_cast(hr)); + return GUID{}; + } + const auto sessionId = info["session_id"].asString(); + if (sessionId.empty()) + { + fprintf(stderr, "[wtcli] No active pane.\n"); + return GUID{}; + } + return GuidFromString(sessionId); +} + +static uint64_t GetFirstWindowId(ITerminalProtocol* server) +{ + Json::Value windows; + CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows); + if (windows.isArray() && !windows.empty()) + return windows[0u]["window_id"].asUInt64(); + return 0; +} + +static uint32_t GetFirstTabId(ITerminalProtocol* server, uint64_t windowId) +{ + Json::Value tabs; + CallJson([&](BSTR* j) { return server->ListTabs(windowId, j); }, tabs); + if (tabs.isArray() && !tabs.empty()) + return tabs[0u]["tab_id"].asUInt(); + return UINT32_MAX; +} + +// Allocate a BSTR from a UTF-8 std::string. +static BSTR Bstr(const std::string& s) +{ + return SysAllocString(winrt::to_hstring(s).c_str()); +} + +// Parse a base-10 unsigned 64-bit integer without throwing (unlike std::stoull, +// which aborts wtcli on non-numeric input). Returns false on empty, non-numeric, +// trailing-garbage, or overflowing input. +static bool TryParseU64(const std::string& s, uint64_t& out) +{ + if (s.empty()) + return false; + uint64_t v = 0; + const auto* first = s.data(); + const auto* last = s.data() + s.size(); + const auto [ptr, ec] = std::from_chars(first, last, v); + if (ec != std::errc{} || ptr != last) + return false; + out = v; + return true; +} + +// ── Main ── + +int main() +{ + winrt::init_apartment(winrt::apartment_type::multi_threaded); + + CLI::App app{ "wtcli - Windows Terminal CLI" }; + app.require_subcommand(0, 1); + + bool jsonMode = false; + int exitCode = 0; + app.add_flag("--json", jsonMode, "Output raw JSON"); + + auto connect = [&]() -> winrt::com_ptr { + auto server = ConnectToTerminal(); + if (!server) + exitCode = 1; + return server; + }; + + // ── list-windows ── + auto* listWindowsCmd = app.add_subcommand("list-windows", "List all windows")->alias("lsw"); + listWindowsCmd->callback([&]() { + auto server = connect(); + if (!server) return; + Json::Value windows; + auto hr = CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows); + if (FAILED(hr)) { fprintf(stderr, "ListWindows failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + { + Json::Value arr(Json::objectValue); + arr["windows"] = windows; + PrintJson(arr); + } + else + { + FormatWindowsHuman(windows); + } + }); + + // ── list-tabs ── + std::string listTabsWindowId; + auto* listTabsCmd = app.add_subcommand("list-tabs", "List tabs in a window")->alias("lst"); + listTabsCmd->add_option("-w,--window-id", listTabsWindowId, "Window ID"); + listTabsCmd->callback([&]() { + auto server = connect(); + if (!server) return; + uint64_t wid = 0; + if (listTabsWindowId.empty()) + { + wid = GetFirstWindowId(server.get()); + if (wid == 0) + { + // 0 is the server's "no filter" sentinel, not a real window id; + // bail rather than silently listing tabs for ALL windows. + fprintf(stderr, "[wtcli] Could not resolve a window (no windows or ListWindows failed)\n"); + exitCode = 1; + return; + } + } + else if (!TryParseU64(listTabsWindowId, wid)) + { + fprintf(stderr, "[wtcli] Invalid --window-id: %s\n", listTabsWindowId.c_str()); + exitCode = 1; + return; + } + Json::Value tabs; + auto hr = CallJson([&](BSTR* j) { return server->ListTabs(wid, j); }, tabs); + if (FAILED(hr)) { fprintf(stderr, "ListTabs failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + { + Json::Value arr(Json::objectValue); + arr["tabs"] = tabs; + PrintJson(arr); + } + else + { + FormatTabsHuman(tabs); + } + }); + + // ── list-panes ── + std::string listPanesTabId, listPanesWindowId; + auto* listPanesCmd = app.add_subcommand("list-panes", "List panes in a tab")->alias("lsp"); + listPanesCmd->add_option("-t,--tab-id", listPanesTabId, "Tab ID"); + listPanesCmd->add_option("-w,--window-id", listPanesWindowId, "Window ID"); + listPanesCmd->callback([&]() { + auto server = connect(); + if (!server) return; + uint64_t wid = 0; + if (!listPanesWindowId.empty() && !TryParseU64(listPanesWindowId, wid)) + { + fprintf(stderr, "[wtcli] Invalid --window-id: %s\n", listPanesWindowId.c_str()); + exitCode = 1; + return; + } + uint32_t tid = UINT32_MAX; + if (!listPanesTabId.empty()) + { + uint64_t t = 0; + if (!TryParseU64(listPanesTabId, t) || t > UINT32_MAX) + { + fprintf(stderr, "[wtcli] Invalid --tab-id: %s\n", listPanesTabId.c_str()); + exitCode = 1; + return; + } + tid = static_cast(t); + } + if (tid == UINT32_MAX) + { + if (wid == 0) + { + wid = GetFirstWindowId(server.get()); + if (wid == 0) + { + fprintf(stderr, "[wtcli] Could not resolve a window (no windows or ListWindows failed)\n"); + exitCode = 1; + return; + } + } + tid = GetFirstTabId(server.get(), wid); + if (tid == UINT32_MAX) + { + fprintf(stderr, "[wtcli] Could not resolve a tab (no tabs or ListTabs failed)\n"); + exitCode = 1; + return; + } + } + Json::Value panes; + auto hr = CallJson([&](BSTR* j) { return server->ListPanes(wid, tid, j); }, panes); + if (FAILED(hr)) { fprintf(stderr, "ListPanes failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + { + Json::Value arr(Json::objectValue); + arr["panes"] = panes; + PrintJson(arr); + } + else + { + FormatPanesHuman(panes); + } + }); + + // ── active-pane ── + auto* activePaneCmd = app.add_subcommand("active-pane", "Show the currently active pane"); + activePaneCmd->callback([&]() { + auto server = connect(); + if (!server) return; + Json::Value info; + auto hr = CallJson([&](BSTR* j) { return server->GetActivePane(j); }, info); + if (FAILED(hr)) { fprintf(stderr, "GetActivePane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + PrintJson(info); + else + FormatActivePaneHuman(info); + }); + + // ── capture-pane ── + std::string capturePaneTarget; + int captureMaxLines = 200; + bool captureLastPrompt = false; + auto* capturePaneCmd = app.add_subcommand("capture-pane", "Capture pane output")->alias("capturep"); + capturePaneCmd->add_option("-t,--target", capturePaneTarget, "Session ID (GUID)"); + capturePaneCmd->add_option("-l,--max-lines", captureMaxLines, "Max lines"); + capturePaneCmd->add_flag("--last-prompt", captureLastPrompt, + "Only return the most recent completed shell prompt (command + output, requires OSC 133 shell integration)"); + capturePaneCmd->callback([&]() { + auto server = connect(); + if (!server) return; + auto sessionId = ResolveSessionId(server.get(), capturePaneTarget); + wil::unique_bstr src{ Bstr(captureLastPrompt ? "last_prompt" : "scrollback") }; + Json::Value output; + auto hr = CallJson([&](BSTR* j) { return server->ReadPaneOutput(sessionId, src.get(), captureMaxLines, j); }, output); + if (FAILED(hr)) { fprintf(stderr, "ReadPaneOutput failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + PrintJson(output); + else + printf("%s\n", output["content"].asString().c_str()); + }); + + // ── pane-status ── + std::string paneStatusTarget; + auto* paneStatusCmd = app.add_subcommand("pane-status", "Show pane process status"); + paneStatusCmd->add_option("-t,--target", paneStatusTarget, "Session ID (GUID)"); + paneStatusCmd->callback([&]() { + auto server = connect(); + if (!server) return; + auto sessionId = ResolveSessionId(server.get(), paneStatusTarget); + Json::Value status; + auto hr = CallJson([&](BSTR* j) { return server->GetProcessStatus(sessionId, j); }, status); + if (FAILED(hr)) { fprintf(stderr, "GetProcessStatus failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + PrintJson(status); + else + FormatPaneStatusHuman(status); + }); + + // ── new-tab ── + std::string newTabCommand, newTabTitle, newTabCwd; + auto* newTabCmd = app.add_subcommand("new-tab", "Create a new tab")->alias("neww"); + newTabCmd->add_option("-c,--command", newTabCommand, "Command to run"); + newTabCmd->add_option("-n,--title", newTabTitle, "Tab title"); + newTabCmd->add_option("-d,--cwd", newTabCwd, "Starting directory"); + newTabCmd->callback([&]() { + auto server = connect(); + if (!server) return; + wil::unique_bstr profile{ Bstr("") }, command{ Bstr(newTabCommand) }, title{ Bstr(newTabTitle) }, cwd{ Bstr(newTabCwd) }; + Json::Value result; + auto hr = CallJson([&](BSTR* j) { + return server->CreateTab(0, profile.get(), command.get(), title.get(), cwd.get(), false, true, j); + }, result); + if (FAILED(hr)) { fprintf(stderr, "CreateTab failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + PrintJson(result); + else + FormatCreatedTabHuman(result); + }); + + // ── split-pane ── + std::string splitPaneTarget, splitPaneCommand, splitPaneDirection; + bool splitHorizontal = false, splitVertical = false; + double splitSize = 0.5; + auto* splitPaneCmd = app.add_subcommand("split-pane", "Split a pane")->alias("splitw"); + splitPaneCmd->add_option("-t,--target", splitPaneTarget, "Session ID (GUID)"); + splitPaneCmd->add_option("-d,--direction", splitPaneDirection, "Split direction: right|left|up|down|auto"); + splitPaneCmd->add_flag("-H,--horizontal", splitHorizontal, "Split horizontally (legacy alias for --direction down)"); + splitPaneCmd->add_flag("-v,--vertical", splitVertical, "Split vertically (legacy alias for --direction right)"); + splitPaneCmd->add_option("-s,--size", splitSize, "Size fraction"); + splitPaneCmd->add_option("-c,--command", splitPaneCommand, "Command to run"); + splitPaneCmd->callback([&]() { + auto server = connect(); + if (!server) return; + auto sessionId = ResolveSessionId(server.get(), splitPaneTarget); + std::string dir; + if (!splitPaneDirection.empty()) + dir = splitPaneDirection; + else if (splitHorizontal) + dir = "down"; + else if (splitVertical) + dir = "right"; + else + dir = "automatic"; + wil::unique_bstr dirB{ Bstr(dir) }, profile{ Bstr("") }, command{ Bstr(splitPaneCommand) }; + Json::Value result; + auto hr = CallJson([&](BSTR* j) { + return server->SplitPane(sessionId, dirB.get(), static_cast(splitSize), profile.get(), command.get(), true, j); + }, result); + if (FAILED(hr)) { fprintf(stderr, "SplitPane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + PrintJson(result); + else + FormatCreatedPaneHuman(result); + }); + + // ── kill-pane ── + std::string killPaneTarget; + auto* killPaneCmd = app.add_subcommand("kill-pane", "Close a pane")->alias("killp"); + killPaneCmd->add_option("-t,--target", killPaneTarget, "Session ID (GUID)"); + killPaneCmd->callback([&]() { + auto server = connect(); + if (!server) return; + auto sessionId = ResolveSessionId(server.get(), killPaneTarget); + auto hr = server->ClosePane(sessionId); + if (FAILED(hr)) { fprintf(stderr, "ClosePane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + { + Json::Value v; + v["ok"] = true; + v["session_id"] = GuidToString(sessionId); + PrintJson(v); + } + else + { + printf("Session %s closed.\n", GuidToString(sessionId).c_str()); + } + }); + + // ── send-keys ── + std::string sendKeysTarget; + std::vector sendKeysArgs; + bool sendKeysRaw = false; + auto* sendKeysCmd = app.add_subcommand("send-keys", "Send keys to a pane")->alias("send"); + sendKeysCmd->add_option("-t,--target", sendKeysTarget, "Session ID (GUID)"); + sendKeysCmd->add_flag("--raw", sendKeysRaw, + "Treat the payload as literal UTF-8 text — skip tmux-style " + "token translation (Enter/Tab/Escape/BSpace/C-x). Use this when " + "forwarding arbitrary agent-supplied text."); + sendKeysCmd->add_option("keys", sendKeysArgs, "Keys to send")->required(); + sendKeysCmd->callback([&]() { + auto server = connect(); + if (!server) return; + auto sessionId = ResolveSessionId(server.get(), sendKeysTarget); + auto text = sendKeysRaw + ? wtcli::JoinAsUtf16(sendKeysArgs) + : wtcli::TranslateKeys(sendKeysArgs); + wil::unique_bstr textB{ SysAllocString(text.c_str()) }; + auto hr = server->SendInput(sessionId, textB.get()); + if (FAILED(hr)) { fprintf(stderr, "SendInput failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + { + Json::Value v; + v["ok"] = true; + v["session_id"] = GuidToString(sessionId); + PrintJson(v); + } + }); + + // ── focus-pane ── + std::string focusPaneTarget; + auto* focusPaneCmd = app.add_subcommand("focus-pane", "Switch focus to a pane")->alias("focusp"); + focusPaneCmd->add_option("-t,--target", focusPaneTarget, "Session ID (GUID)"); + focusPaneCmd->callback([&]() { + auto server = connect(); + if (!server) return; + auto sessionId = ResolveSessionId(server.get(), focusPaneTarget); + auto hr = server->FocusPane(sessionId); + if (FAILED(hr)) { fprintf(stderr, "FocusPane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (jsonMode) + { + Json::Value v; + v["ok"] = true; + v["session_id"] = GuidToString(sessionId); + PrintJson(v); + } + else + { + printf("Focused pane %s.\n", GuidToString(sessionId).c_str()); + } + }); + + // ── test-pipe ── + auto* testPipeCmd = app.add_subcommand("test-pipe", "Test connection to Windows Terminal"); + testPipeCmd->callback([&]() { + printf("Connecting to Windows Terminal...\n"); + auto server = connect(); + if (!server) { fprintf(stderr, "Connection failed.\n"); return; } + printf("Connected and authenticated!\n\n"); + + Json::Value windows; + if (SUCCEEDED(CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows))) + { + Json::Value arr(Json::objectValue); + arr["windows"] = windows; + printf("list_windows:\n"); + PrintJson(arr); + } + printf("\n"); + + Json::Value caps; + if (SUCCEEDED(CallJson([&](BSTR* j) { return server->GetCapabilities(j); }, caps))) + { + printf("get_capabilities:\n"); + PrintJson(caps); + } + }); + + // ── info ── + auto* infoCmd = app.add_subcommand("info", "Show connection info"); + infoCmd->callback([&]() { + wchar_t clsid[128]{}; + auto hasClsid = GetEnvironmentVariableW(L"WT_COM_CLSID", clsid, ARRAYSIZE(clsid)) > 0; + + std::string version; + auto server = ConnectToTerminal(nullptr, &version); + + Json::Value methods(Json::arrayValue); + if (server) + { + CallJson([&](BSTR* j) { return server->GetCapabilities(j); }, methods); + } + + if (jsonMode) + { + Json::Value v; + if (hasClsid) + v["com_clsid"] = winrt::to_string(winrt::hstring{ clsid }); + v["connected"] = (server != nullptr); + if (!version.empty()) + v["protocol_version"] = version; + v["methods"] = methods.isArray() ? methods : Json::Value(Json::arrayValue); + PrintJson(v); + } + else + { + printf("Windows Terminal Protocol Info\n"); + printf("========================================\n"); + if (hasClsid) + printf(" COM CLSID: %ls\n", clsid); + else + printf(" COM CLSID: (not set)\n"); + printf("\n"); + if (!server) + { + printf(" Connection: FAILED\n"); + } + else + { + printf(" Connection: OK\n"); + if (!version.empty()) + printf(" Protocol: %s\n", version.c_str()); + printf("\n"); + if (methods.isArray() && methods.size() > 0) + { + printf(" Methods: %u supported\n", methods.size()); + for (const auto& m : methods) + printf(" - %s\n", m.asString().c_str()); + } + } + } + + if (!server) + exitCode = 1; + }); + + // ── wait-for ── + std::string waitForTarget; + int waitInterval = 500; + int waitTimeout = 0; + auto* waitForCmd = app.add_subcommand("wait-for", "Wait for a pane to exit"); + waitForCmd->add_option("-t,--target", waitForTarget, "Session ID (GUID)")->required(); + waitForCmd->add_option("--interval", waitInterval, "Poll interval (ms)"); + waitForCmd->add_option("--timeout", waitTimeout, "Timeout (seconds, 0=forever)"); + waitForCmd->callback([&]() { + auto server = connect(); + if (!server) return; + auto sessionId = ResolveSessionId(server.get(), waitForTarget); + auto start = std::chrono::steady_clock::now(); + + while (true) + { + Json::Value status; + auto hr = CallJson([&](BSTR* j) { return server->GetProcessStatus(sessionId, j); }, status); + if (FAILED(hr)) + { + fprintf(stderr, "GetProcessStatus failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } + if (status["state"].asString() == "exited") + { + if (jsonMode) + { + Json::Value v; + v["state"] = "exited"; + if (status.isMember("exit_code")) + v["exit_code"] = status["exit_code"].asInt(); + PrintJson(v); + } + else + { + printf("Process exited"); + if (status.isMember("has_exit_code") ? status["has_exit_code"].asBool() : status.isMember("exit_code")) + printf(" (code %d)", status["exit_code"].asInt()); + printf("\n"); + } + return; + } + + if (waitTimeout > 0) + { + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + if (elapsed >= waitTimeout) + { + fprintf(stderr, "Timeout waiting for pane %s\n", waitForTarget.c_str()); + exitCode = 1; + return; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(waitInterval)); + } + }); + + // ── set-env ── + std::string setEnvShell = "powershell"; + auto* setEnvCmd = app.add_subcommand("set-env", "Print env setup commands")->alias("setenv"); + setEnvCmd->add_option("-s,--shell", setEnvShell, "Shell: powershell, bash, cmd"); + setEnvCmd->callback([&]() { + wchar_t clsid[128]{}; + GetEnvironmentVariableW(L"WT_COM_CLSID", clsid, ARRAYSIZE(clsid)); + auto cl = winrt::to_string(winrt::hstring{ clsid }); + + if (setEnvShell == "powershell" || setEnvShell == "pwsh") + { + if (!cl.empty()) printf("$env:WT_COM_CLSID = '%s'\n", cl.c_str()); + } + else if (setEnvShell == "bash" || setEnvShell == "sh" || setEnvShell == "zsh") + { + if (!cl.empty()) printf("export WT_COM_CLSID='%s'\n", cl.c_str()); + } + else if (setEnvShell == "cmd") + { + if (!cl.empty()) printf("set WT_COM_CLSID=%s\n", cl.c_str()); + } + }); + + // ── publish ── + // Low-level "pass this JSON through to SendEvent verbatim" escape hatch. + std::string publishJson; + auto* publishCmd = app.add_subcommand("publish", "Forward raw JSON to SendEvent"); + publishCmd->add_option("json", publishJson, "Full event JSON (e.g. {\"method\":\"autofix_state\",\"params\":{...}})")->required(); + publishCmd->callback([&]() { + auto server = connect(); + if (!server) return; + wil::unique_bstr evt{ Bstr(publishJson) }; + auto hr = server->SendEvent(evt.get()); + if (FAILED(hr)) { fprintf(stderr, "publish failed: 0x%08X\n", static_cast(hr)); exitCode = 1; } + }); + + // ── send-event ── + std::string sendEventType, sendEventJson, sendEventPaneTarget; + auto* sendEventCmd = app.add_subcommand("send-event", "Publish an event to all listeners")->alias("se"); + sendEventCmd->add_option("-p,--pane", sendEventPaneTarget, "Source session ID (GUID)"); + sendEventCmd->add_option("-e,--event", sendEventType, "Event type (e.g. agent.task.started)")->required(); + sendEventCmd->add_option("json", sendEventJson, "Event params as JSON object"); + sendEventCmd->callback([&]() { + auto server = connect(); + if (!server) return; + std::string resolvedSessionId; + if (!sendEventPaneTarget.empty()) + { + resolvedSessionId = sendEventPaneTarget; + } + else + { + // Fall back to the active pane as the event source. If there is no + // active pane, bail rather than sending with an all-zero GUID, + // which would silently misroute the event. + const auto activeSid = ResolveSessionId(server.get(), ""); + if (IsEqualGUID(activeSid, GUID{})) + { + fprintf(stderr, "[wtcli] send-event: no --pane given and no active pane to use as the event source.\n"); + exitCode = 1; + return; + } + resolvedSessionId = GuidToString(activeSid); + } + Json::Value evt; + if (!wtcli::BuildSendEventJson(sendEventType, sendEventJson, resolvedSessionId, evt)) + { + fprintf(stderr, "Invalid JSON for --json: value must be a JSON object (e.g. '{\"key\":\"val\"}')\n"); + exitCode = 1; + return; + } + Json::StreamWriterBuilder wb; + wb["indentation"] = ""; + wil::unique_bstr evtB{ Bstr(Json::writeString(wb, evt)) }; + auto hr = server->SendEvent(evtB.get()); + if (FAILED(hr)) { fprintf(stderr, "SendEvent failed: 0x%08X\n", static_cast(hr)); exitCode = 1; } + }); + + // ── listen ── + std::string listenTarget; + std::string listenEventFilter; + auto* listenCmd = app.add_subcommand("listen", "Stream real-time events from Windows Terminal"); + listenCmd->add_option("-t,--target", listenTarget, "Filter by session ID (GUID)"); + listenCmd->add_option("--event", listenEventFilter, "Filter by event type (supports trailing wildcard, e.g. agent.*)"); + listenCmd->callback([&]() { + auto server = connect(); + if (!server) { exitCode = 1; return; } + + static HANDLE s_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!s_stopEvent) + { + fprintf(stderr, "[wtcli] listen: failed to create stop event (0x%08X)\n", GetLastError()); + exitCode = 1; + return; + } + SetConsoleCtrlHandler([](DWORD) -> BOOL { + SetEvent(s_stopEvent); + return TRUE; + }, TRUE); + + if (!jsonMode) + fprintf(stderr, "Listening for events... (Ctrl-C to stop)\n"); + + // EventSink is born with _ref == 1, so attach() (adopt, no AddRef) hands + // that reference to the com_ptr. RAII then Releases on every exit path -- + // exception-safe and robust against future early-returns, no manual + // Release to forget. + winrt::com_ptr sink; + sink.attach(new EventSink([&](const std::string& eventUtf8) { + if (!wtcli::MatchesEventFilter(eventUtf8, listenTarget, listenEventFilter)) + return; + printf("%s\n", eventUtf8.c_str()); + fflush(stdout); + })); + + auto hr = server->Subscribe(sink.get()); + if (FAILED(hr)) + { + fprintf(stderr, "Subscribe failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } + + WaitForSingleObject(s_stopEvent, INFINITE); + server->Unsubscribe(); + // s_stopEvent is intentionally NOT closed: it is static and still + // referenced by the registered Ctrl-C handler (a non-capturing lambda + // that can only reach it via the static), so closing it would leave the + // handler pointing at an invalid handle. It is reclaimed at process exit. + }); + + // ── Default (no subcommand) ── + app.callback([&]() { + if (app.get_subcommands().empty()) + { + printf("wtcli - Windows Terminal CLI\n\n"); + printf("Usage: wtcli [--json] \n\n"); + printf("Run 'wtcli --help' for available subcommands.\n"); + } + }); + + try + { + app.parse(__argc, __argv); + } + catch (const CLI::ParseError& e) + { + return app.exit(e); + } + + return exitCode; +} diff --git a/src/tools/wtcli/wtcli.vcxproj b/src/tools/wtcli/wtcli.vcxproj new file mode 100644 index 00000000000..97eb143381a --- /dev/null +++ b/src/tools/wtcli/wtcli.vcxproj @@ -0,0 +1,50 @@ + + + + {8A3B5E2F-1C4D-4F6A-9B8C-0D1E2F3A4B5C} + Win32Proj + wtcli + wtcli + wtcli + Application + false + + + true + + + + + + + + + + + + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + $(IntDir)..\OpenConsoleProxy;%(AdditionalIncludeDirectories) + + + Console + ole32.lib;WindowsApp.lib;%(AdditionalDependencies) + + + + + + {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} + false + + + + + diff --git a/src/tools/wtcli/wtcli_functions.h b/src/tools/wtcli/wtcli_functions.h new file mode 100644 index 00000000000..3a5a6c242e8 --- /dev/null +++ b/src/tools/wtcli/wtcli_functions.h @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Extracted pure functions from wtcli for fuzzing and testability. +// These functions have no COM/WinRT dependencies and can be called +// from a LibFuzzer harness. + +#pragma once + +#include +#include +#include + +#include +#include + +namespace wtcli +{ + // Concatenate positional args as literal UTF-8 → UTF-16 text without + // any tmux-style token interpretation. Use this when the caller's intent + // is "send these exact characters" (e.g. wta forwarding agent-supplied + // text), so payloads like the literal word "Enter" / "Tab" / "C-c" are + // not silently rewritten into control bytes. + inline std::wstring JoinAsUtf16(const std::vector& parts) + { + std::wstring result; + bool first = true; + for (const auto& p : parts) + { + // Space-separate consecutive args so an unquoted human invocation + // like `wtcli send-keys --raw hello world` reaches the pane as + // "hello world" rather than "helloworld". wta callers pass a + // single positional via `--`, so they are unaffected. + if (!first) + { + result += L' '; + } + first = false; + if (p.empty()) + continue; + const int wlen = MultiByteToWideChar(CP_UTF8, 0, p.data(), static_cast(p.size()), nullptr, 0); + if (wlen > 0) + { + const size_t prev = result.size(); + result.resize(prev + static_cast(wlen)); + MultiByteToWideChar(CP_UTF8, 0, p.data(), static_cast(p.size()), result.data() + prev, wlen); + } + } + return result; + } + + // Translate tmux-style key names to the byte stream that should be sent + // to a pane. Recognized tokens: Enter / Space / Tab / Escape (alias Esc) / + // BSpace / C-a..C-z. Unrecognized tokens are passed through as UTF-8 → + // UTF-16 text. "Enter" maps to a single CR — SendProtocolInput downstream + // translates LF to CR as well, so emitting CRLF here would produce a + // double-CR (two Enter keypresses). + inline std::wstring TranslateKeys(const std::vector& keys) + { + std::wstring result; + for (const auto& key : keys) + { + if (key == "Enter" || key == "enter") + result += L"\r"; + else if (key == "Space" || key == "space") + result += L" "; + else if (key == "Tab" || key == "tab") + result += L"\t"; + else if (key == "Escape" || key == "escape" || key == "Esc" || key == "esc") + result += L"\x1b"; + else if (key == "BSpace" || key == "bspace") + result += L"\b"; + else if (key == "C-c") + result += L"\x03"; + else if (key == "C-d") + result += L"\x04"; + else if (key == "C-z") + result += L"\x1a"; + else if (key == "C-l") + result += L"\x0c"; + else if (key.size() == 3 && key[0] == 'C' && key[1] == '-' && key[2] >= 'a' && key[2] <= 'z') + result += static_cast(key[2] - 'a' + 1); + else if (!key.empty()) + { + const int wlen = MultiByteToWideChar(CP_UTF8, 0, key.data(), static_cast(key.size()), nullptr, 0); + if (wlen > 0) + { + const size_t prev = result.size(); + result.resize(prev + static_cast(wlen)); + MultiByteToWideChar(CP_UTF8, 0, key.data(), static_cast(key.size()), result.data() + prev, wlen); + } + } + } + return result; + } + + + // Build the standard JSON envelope the COM server expects for an + // `agent_event`. The caller provides the event name, an optional JSON + // object string containing extra params, and the source pane Guid; this + // function folds in `pane_id` and emits the wrapped + // `{ type, method, params }` object in `outEvt`. + // + // Returns true on success and populates `outEvt`. + // Returns false and leaves `outEvt` untouched if `paramsJson` is + // non-empty but not a valid JSON object. + // + // |eventType| — required event name (e.g. "agent.task.started") + // |paramsJson| — optional JSON object string with extra params + // |sessionId| — source pane Guid as a string (already resolved by caller). + // Named `sessionId` for backwards compatibility with the + // old per-pane "session_id" terminology; the value is + // the WT pane GUID, which goes into `params["pane_id"]` + // — matching the rename in TerminalPage.cpp for + // connection_state / vt_sequence events. + inline bool BuildSendEventJson( + const std::string& eventType, + const std::string& paramsJson, + const std::string& sessionId, + Json::Value& outEvt) + { + Json::Value params; + if (!paramsJson.empty()) + { + Json::CharReaderBuilder rb; + std::string errs; + std::istringstream ss(paramsJson); + if (!Json::parseFromStream(rb, ss, ¶ms, &errs) || !params.isObject()) + { + return false; + } + } + + params["event"] = eventType; + params["pane_id"] = sessionId; + + outEvt["type"] = "event"; + outEvt["method"] = "agent_event"; + outEvt["params"] = params; + return true; + } + + // Check whether an event JSON string passes the session_id and event type + // filters used by the "listen" command. + // + // Returns true if the event should be emitted (matches filters or filters + // are empty). Returns true on parse failure to match original behavior + // (unparseable events are passed through). + // + // |eventTypeFilter| supports a trailing wildcard: "agent.*" matches + // "agent.task.started". + inline bool MatchesEventFilter( + const std::string& eventJson, + const std::string& sessionIdFilter, + const std::string& eventTypeFilter) + { + if (sessionIdFilter.empty() && eventTypeFilter.empty()) + { + return true; + } + + Json::Value ev; + Json::CharReaderBuilder rb; + std::string errs; + std::istringstream ss(eventJson); + if (!Json::parseFromStream(rb, ss, &ev, &errs)) + { + return true; + } + + // Event JSON must be an object with a "params" object inside. + // Reject structurally invalid events when filters are active — + // missing fields can't match any filter. + if (!ev.isObject() || !ev.isMember("params") || !ev["params"].isObject()) + { + return false; + } + + if (!sessionIdFilter.empty()) + { + // Look for pane_id (current name) first, then fall back to + // session_id (old name) so older listen consumers / events + // produced before the rename keep matching during a + // partial upgrade. + auto paneId = ev["params"].get("pane_id", "").asString(); + if (paneId.empty()) + { + paneId = ev["params"].get("session_id", "").asString(); + } + if (paneId != sessionIdFilter) + { + return false; + } + } + + if (!eventTypeFilter.empty()) + { + auto eventType = ev["params"].get("event", "").asString(); + if (eventTypeFilter.back() == '*') + { + auto prefix = eventTypeFilter.substr(0, eventTypeFilter.size() - 1); + if (eventType.substr(0, prefix.size()) != prefix) + { + return false; + } + } + else if (eventType != eventTypeFilter) + { + return false; + } + } + + return true; + } +} From 74af0a307cfec822e745816842a8ac94af21fb05 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Mon, 22 Jun 2026 10:12:31 -0700 Subject: [PATCH 02/26] missed appxmanifest and vcxproj --- src/cascadia/CascadiaPackage/Package-Dev.appxmanifest | 3 +++ src/cascadia/TerminalApp/dll/TerminalApp.vcxproj | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index 4a3153076c8..3045db61b64 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -113,6 +113,8 @@ + + @@ -122,6 +124,7 @@ + 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 + From e4d1064aa22f7ad1ee00a17f5abfdfd0ac5495c3 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Mon, 22 Jun 2026 13:46:57 -0700 Subject: [PATCH 03/26] another vcxproj fix --- src/cascadia/TerminalApp/TerminalAppLib.vcxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index f3c6499536e..04826fa67c1 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -389,6 +389,10 @@ you also update all the consumers --> + + {F1A2B3C4-D5E6-4F7A-8B9C-0D1E2F3A4B5D} + false + {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE} false From 0899ce291c44a5a41990374802f6568539e9e554 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 24 Jun 2026 10:40:59 -0700 Subject: [PATCH 04/26] read last prompt fix --- .../TerminalApp/TerminalPage.Protocol.cpp | 4 +-- src/cascadia/TerminalControl/ControlCore.cpp | 34 +++++++++++++++++++ src/cascadia/TerminalControl/ControlCore.h | 1 + src/cascadia/TerminalControl/ControlCore.idl | 1 + src/cascadia/TerminalControl/TermControl.cpp | 4 +++ src/cascadia/TerminalControl/TermControl.h | 1 + src/cascadia/TerminalControl/TermControl.idl | 1 + .../TerminalProtocolComServer.cpp | 4 ++- 8 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index 2ffdfe6bb81..9890c6fd207 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -473,7 +473,7 @@ namespace winrt::TerminalApp::implementation if (!pane) co_return result; - _CreateNewTabFromPane(pane, -1, /*openInBackground=*/background); + _CreateNewTabFromPane(pane, static_cast(-1)); _tabContent.UpdateLayout(); // Force synchronous terminal initialization if (_tabs.Size() == 0) @@ -533,7 +533,7 @@ namespace winrt::TerminalApp::implementation const auto newPanePid = _getPidFromPane(newPane); auto newPaneRef = newPane; // copy shared_ptr before move - _SplitPane(tabImpl, direction, size, std::move(newPane), /*focusNewPane=*/!background); + _SplitPane(tabImpl, direction, size, std::move(newPane)); _tabContent.UpdateLayout(); // Force synchronous terminal initialization result.TabId = tabIdx; 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/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index 74920db137e..8126af1579e 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -516,7 +516,9 @@ try info.WindowId = props.WindowId(); info.Title = props.WindowNameForDisplay(); info.IsFocused = (host.get() == mostRecent); - info.TabCount = logic.TabCount(); + + const auto page = _getPage(host.get()); + info.TabCount = page ? page.TabCount() : 0; arr.append(_toJson(info)); } From e31ceab487754899bb461be412061b38d67e2d6e Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Mon, 6 Jul 2026 15:15:04 -0700 Subject: [PATCH 05/26] real clsids --- .../CascadiaPackage/Package-Can.appxmanifest | 3 + .../CascadiaPackage/Package-Dev.appxmanifest | 6 +- .../CascadiaPackage/Package-Pre.appxmanifest | 3 + .../CascadiaPackage/Package.appxmanifest | 3 + .../TerminalProtocolComServer.cpp | 25 ------ .../TerminalProtocolComServer.h | 10 +-- .../WindowsTerminal/WindowEmperor.cpp | 17 +--- src/cascadia/WindowsTerminal/WindowEmperor.h | 2 - src/host/proxy/ITerminalProtocol.idl | 8 +- src/tools/wtcli/main.cpp | 77 ++++--------------- 10 files changed, 37 insertions(+), 117 deletions(-) diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index 8335a59f420..bef30deac9c 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -113,6 +113,8 @@ + + @@ -122,6 +124,7 @@ + diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index 3045db61b64..ff36d84bcd5 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -113,8 +113,8 @@ - - + + @@ -124,7 +124,7 @@ - + diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index 497923da40a..fd642545b1f 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -202,6 +202,8 @@ + + @@ -211,6 +213,7 @@ + diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index 679f3b9a526..2eaf9a1fc2e 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -202,6 +202,8 @@ + + @@ -211,6 +213,7 @@ + diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index 8126af1579e..d75dd04ed38 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -406,28 +406,6 @@ void TerminalProtocolComServer::s_NotifyEventToComClients(const std::string& eve // ITerminalProtocol — Meta // ============================================================================ -STDMETHODIMP TerminalProtocolComServer::Authenticate(BSTR /*token*/, BSTR* resultJson) -try -{ - RETURN_HR_IF_NULL(E_POINTER, resultJson); - *resultJson = nullptr; - RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); - - // DEV BYPASS: always authenticate — credential plumbing not yet implemented. - _authenticated = true; - - // Register for event delivery on successful authentication. - _addInstance(); - - Json::Value v; - v["authenticated"] = _authenticated; - // 2.2 — SendInput restored on the COM surface; pane identifiers remain GUIDs. - v["protocol_version"] = "2.2"; - *resultJson = _bstrFromJson(v); - return S_OK; -} -CATCH_RETURN() - STDMETHODIMP TerminalProtocolComServer::GetCapabilities(BSTR* json) try { @@ -905,7 +883,6 @@ STDMETHODIMP TerminalProtocolComServer::Subscribe(ITerminalProtocolEventSink* si try { RETURN_HR_IF(E_INVALIDARG, !sink); - RETURN_HR_IF(E_ACCESSDENIED, !_authenticated); // Store the sink as an agile reference so it can be resolved + called from // any apartment (the VT-event fan-out runs on the UI/STA thread, while the @@ -934,8 +911,6 @@ STDMETHODIMP TerminalProtocolComServer::Unsubscribe() STDMETHODIMP TerminalProtocolComServer::SendEvent(BSTR eventJson) try { - RETURN_HR_IF(E_ACCESSDENIED, !_authenticated); - const auto eventH = _hstr(eventJson); auto jsonStr = winrt::to_string(eventH); Json::Value evt; diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h index 350dc84489c..8e731e14e42 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -14,13 +14,13 @@ // Per-brand CLSIDs — same pattern as CTerminalHandoff. Reused unchanged from the // previous WinRT/MBM server, so WT_COM_CLSID discovery on the client is identical. #if defined(WT_BRANDING_RELEASE) -#define __CLSID_TerminalProtocolServer "A2E4F6B8-1C3D-4E5F-A6B7-C8D9E0F1A2B3" +#define __CLSID_TerminalProtocolServer "832FDEC7-AA6F-4BAB-85FA-A491405638FC" #elif defined(WT_BRANDING_PREVIEW) -#define __CLSID_TerminalProtocolServer "B3F5A7C9-2D4E-4F6A-B7C8-D9E0F1A2B3C4" +#define __CLSID_TerminalProtocolServer "D77C8A1A-83C0-42FC-BADF-9BE82E2A1624" #elif defined(WT_BRANDING_CANARY) -#define __CLSID_TerminalProtocolServer "C4A6B8D0-3E5F-4A7B-C8D9-E0F1A2B3C4D5" +#define __CLSID_TerminalProtocolServer "264DE65B-F597-4183-8A76-6A039A604725" #else -#define __CLSID_TerminalProtocolServer "D5B7C9E1-4F6A-4B8C-D9E0-F1A2B3C4D5E6" +#define __CLSID_TerminalProtocolServer "AD9425AA-1722-4E7B-A451-AA1D09106E83" #endif class WindowEmperor; @@ -39,7 +39,6 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< ~TerminalProtocolComServer(); // ── ITerminalProtocol ── - STDMETHODIMP Authenticate(BSTR token, BSTR* resultJson) override; STDMETHODIMP GetCapabilities(BSTR* json) override; STDMETHODIMP GetActivePane(BSTR* json) override; STDMETHODIMP ListWindows(BSTR* json) override; @@ -72,7 +71,6 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< static void s_NotifyEventToComClients(const std::string& eventJson); private: - bool _authenticated = false; // Per-instance event sink, stored as an agile reference so it can be // resolved + called from any apartment (set via Subscribe, cleared via diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 002a5976bcf..394d0d8bc14 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -1745,19 +1745,6 @@ void WindowEmperor::_initializeProtocolServer() { // Register COM class factory for cross-process access (runs on MTA thread). TerminalProtocolComServer::s_setEmperor(this); - if (SUCCEEDED_LOG(TerminalProtocolComServer::s_StartListening())) - { - // Stringify the CLSID so child processes can discover us via CoCreateInstance. - wil::unique_cotaskmem_string clsidStr; - if (SUCCEEDED(StringFromCLSID(__uuidof(TerminalProtocolComServer), &clsidStr)) - && clsidStr) - { - _comClsid = clsidStr.get(); - SetEnvironmentVariableW(L"WT_COM_CLSID", _comClsid.c_str()); - } - } - - OutputDebugStringA(fmt::format("WT Protocol Server started\n WT_COM_CLSID={}\n", - winrt::to_string(_comClsid)) - .c_str()); + 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 4bbaeacba9f..2ea6245dd0b 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -55,7 +55,6 @@ class WindowEmperor void OpenWindow(const winrt::hstring& name); // Protocol server access - const std::wstring& GetComClsid() const noexcept { return _comClsid; } const std::vector>& GetWindows() const noexcept { return _windows; } AppHost* GetMostRecentWindow() const noexcept { return _mostRecentWindow(); } @@ -99,7 +98,6 @@ class WindowEmperor std::vector> _windows; // Protocol server for AI CLI integration - std::wstring _comClsid; // Stringified CLSID for WT_COM_CLSID env var void _initializeProtocolServer(); std::vector _hotkeys; NOTIFYICONDATA _notificationIcon{}; diff --git a/src/host/proxy/ITerminalProtocol.idl b/src/host/proxy/ITerminalProtocol.idl index efd379c9666..c623b24de14 100644 --- a/src/host/proxy/ITerminalProtocol.idl +++ b/src/host/proxy/ITerminalProtocol.idl @@ -21,7 +21,7 @@ import "unknwn.idl"; // this cross-process to push events (each event is a JSON string). [ object, - uuid(3D8F4B26-5C7E-4A9B-B1D0-2F5A7C9E1B4D), + uuid(DD47E357-3D5E-4EFD-901B-C02CE6C45B7D), pointer_default(unique) ] interface ITerminalProtocolEventSink : IUnknown { @@ -29,16 +29,14 @@ import "unknwn.idl"; }; // Terminal protocol — implemented by WindowsTerminal.exe, activated by the -// client via CoCreateInstance(CLSCTX_LOCAL_SERVER) under the existing -// per-branding CLSID (discovered through the WT_COM_CLSID env var). +// client via CoCreateInstance(CLSCTX_LOCAL_SERVER) with the per-brand CLSID. [ object, - uuid(9C7E2A14-3B5D-4F8A-A2C9-1E4F6B8D0A3C), + uuid(D999831E-7EBD-4B14-9D2F-8B09417582D4), pointer_default(unique) ] interface ITerminalProtocol : IUnknown { // ── Meta ── - HRESULT Authenticate([in] BSTR token, [out, retval] BSTR* resultJson); HRESULT GetCapabilities([out, retval] BSTR* json); // ── Queries (complex results as JSON) ── diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index a8e4e3b8ecc..b95f8d62713 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -68,74 +68,29 @@ struct EventSink : ITerminalProtocolEventSink // ── Helpers ── -static winrt::com_ptr ConnectToTerminal(bool* outAuthenticated = nullptr, std::string* outVersion = nullptr) +// Per-brand CLSID — matches the server's compile-time selection. +#if defined(WT_BRANDING_RELEASE) +static constexpr CLSID CLSID_TerminalProtocol = { 0x832FDEC7, 0xAA6F, 0x4BAB, { 0x85, 0xFA, 0xA4, 0x91, 0x40, 0x56, 0x38, 0xFC } }; +#elif defined(WT_BRANDING_PREVIEW) +static constexpr CLSID CLSID_TerminalProtocol = { 0xD77C8A1A, 0x83C0, 0x42FC, { 0xBA, 0xDF, 0x9B, 0xE8, 0x2E, 0x2A, 0x16, 0x24 } }; +#elif defined(WT_BRANDING_CANARY) +static constexpr CLSID CLSID_TerminalProtocol = { 0x264DE65B, 0xF597, 0x4183, { 0x8A, 0x76, 0x6A, 0x03, 0x9A, 0x60, 0x47, 0x25 } }; +#else +static constexpr CLSID CLSID_TerminalProtocol = { 0xAD9425AA, 0x1722, 0x4E7B, { 0xA4, 0x51, 0xAA, 0x1D, 0x09, 0x10, 0x6E, 0x83 } }; +#endif + +static winrt::com_ptr ConnectToTerminal(std::string* outVersion = nullptr) { - wchar_t clsid[128]{}; - if (!GetEnvironmentVariableW(L"WT_COM_CLSID", clsid, ARRAYSIZE(clsid))) - { - fprintf(stderr, "[wtcli] WT_COM_CLSID not set. Must run inside a Windows Terminal pane.\n"); - return nullptr; - } - - CLSID cls{}; - if (FAILED(CLSIDFromString(clsid, &cls))) - { - fprintf(stderr, "[wtcli] Invalid CLSID: %ls\n", clsid); - return nullptr; - } - winrt::com_ptr server; - auto hr = CoCreateInstance(cls, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(ITerminalProtocol), server.put_void()); + auto hr = CoCreateInstance(CLSID_TerminalProtocol, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(ITerminalProtocol), server.put_void()); if (FAILED(hr)) { fprintf(stderr, "[wtcli] Connection failed: 0x%08X\n", static_cast(hr)); return nullptr; } - BSTR rawAuth = nullptr; - hr = server->Authenticate(nullptr, &rawAuth); - bool parsed = false; - bool authenticated = false; - std::string version; - if (SUCCEEDED(hr) && rawAuth) - { - Json::Value v; - Json::CharReaderBuilder rb; - std::string errs; - auto s = winrt::to_string(winrt::hstring{ rawAuth }); - std::istringstream ss(s); - if (Json::parseFromStream(rb, ss, &v, &errs)) - { - parsed = true; - authenticated = v["authenticated"].asBool(); - version = v["protocol_version"].asString(); - } - } - if (rawAuth) - SysFreeString(rawAuth); - - if (FAILED(hr)) - { - fprintf(stderr, "[wtcli] Authentication failed: 0x%08X\n", static_cast(hr)); - return nullptr; - } - if (!parsed) - { - // Success HRESULT but a null/malformed auth payload is a broken - // server contract — don't misreport it as a server rejection. - fprintf(stderr, "[wtcli] Authentication response missing or malformed (server contract error)\n"); - return nullptr; - } - if (!authenticated) - { - fprintf(stderr, "[wtcli] Authentication rejected by server\n"); - return nullptr; - } - - if (outAuthenticated) - *outAuthenticated = authenticated; if (outVersion) - *outVersion = version; + *outVersion = "2.2"; return server; } @@ -593,7 +548,7 @@ int main() printf("Connecting to Windows Terminal...\n"); auto server = connect(); if (!server) { fprintf(stderr, "Connection failed.\n"); return; } - printf("Connected and authenticated!\n\n"); + printf("Connected!\n\n"); Json::Value windows; if (SUCCEEDED(CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows))) @@ -620,7 +575,7 @@ int main() auto hasClsid = GetEnvironmentVariableW(L"WT_COM_CLSID", clsid, ARRAYSIZE(clsid)) > 0; std::string version; - auto server = ConnectToTerminal(nullptr, &version); + auto server = ConnectToTerminal(&version); Json::Value methods(Json::arrayValue); if (server) From a9db78dfb7ef7f0a26afe18c3f41e853e56ae050 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Mon, 6 Jul 2026 15:50:39 -0700 Subject: [PATCH 06/26] remove event sink --- .../CascadiaPackage/Package-Can.appxmanifest | 1 - .../CascadiaPackage/Package-Dev.appxmanifest | 1 - .../CascadiaPackage/Package-Pre.appxmanifest | 1 - .../CascadiaPackage/Package.appxmanifest | 1 - src/cascadia/WindowsTerminal/AppHost.cpp | 4 - .../TerminalProtocolComServer.cpp | 253 ------------------ .../TerminalProtocolComServer.h | 44 +-- .../WindowsTerminal/WindowEmperor.cpp | 5 - src/host/proxy/ITerminalProtocol.idl | 16 -- src/tools/wtcli/ft_fuzzer/fuzzmain.cpp | 68 +---- src/tools/wtcli/main.cpp | 146 ---------- src/tools/wtcli/wtcli_functions.h | 118 -------- 12 files changed, 4 insertions(+), 654 deletions(-) diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index bef30deac9c..423c1081f1c 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -114,7 +114,6 @@ - diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index ff36d84bcd5..70ef00e1432 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -114,7 +114,6 @@ - diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index fd642545b1f..7109b7e8ae5 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -203,7 +203,6 @@ - diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index 2eaf9a1fc2e..2aae6d1e049 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -203,7 +203,6 @@ - diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index dc53073a004..ad6fd912bb2 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -1253,10 +1253,6 @@ safe_void_coroutine AppHost::_WindowInitializedHandler(const winrt::Windows::Fou { _isWindowInitialized = WindowInitializedState::Initializing; - // Re-run page-events registration now that the TerminalPage is - // actually constructed. Ensures this window's events reach COM clients. - TerminalProtocolComServer::s_OnWindowAdded(this); - // GH#11561: We're totally done being initialized. Resize the window to // match the initial settings, and then call ShowWindow to finally make us // visible. diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index d75dd04ed38..b1b76be1b8d 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -32,10 +32,6 @@ static std::shared_mutex g_mtx; static std::thread g_comMtaThread; static wil::unique_event g_comMtaStop; -// Static instance tracking for event delivery to COM clients -std::mutex TerminalProtocolComServer::s_instancesMutex; -std::vector TerminalProtocolComServer::s_instances; - void TerminalProtocolComServer::s_setEmperor(WindowEmperor* emperor) noexcept { s_emperor = emperor; @@ -116,26 +112,6 @@ HRESULT TerminalProtocolComServer::s_StopListening() return S_OK; } -TerminalProtocolComServer::~TerminalProtocolComServer() -{ - _removeInstance(); -} - -void TerminalProtocolComServer::_addInstance() -{ - std::lock_guard lock{ s_instancesMutex }; - if (_instanceRegistered) - return; - _instanceRegistered = true; - s_instances.push_back(this); -} - -void TerminalProtocolComServer::_removeInstance() -{ - std::lock_guard lock{ s_instancesMutex }; - std::erase(s_instances, this); -} - // ============================================================================ // Helpers // ============================================================================ @@ -281,127 +257,6 @@ static winrt::hstring _hstr(BSTR b) return b ? winrt::hstring{ b } : winrt::hstring{}; } -void TerminalProtocolComServer::_ensurePageEventsRegistered() -{ - if (!s_emperor) - return; - - // Per-TerminalPage registration tracking. Each window gets its - // ProtocolVtSequenceReceived wired exactly once, so events from any - // window — not just the lexically-first one — reach the COM fan-out. - // - // Key on `winrt::weak_ref` rather than `AppHost*`: - // - A closed window's AppHost is destructed and the same memory - // address may later be reused by a freshly-created AppHost. With a - // raw-pointer key the new window would be misidentified as - // "already registered" and silently skipped (ABA bug). - // - `weak_ref` tracks the WinRT object's actual identity; dead - // entries surface as `weak_ref::get()` returning null, and new - // pages are recognized as distinct even if they share an address - // with a defunct one. - static std::mutex s_regMutex; - static std::vector> s_registered; - - std::lock_guard lock{ s_regMutex }; - - // Prune dead entries (page destructed → weak_ref returns null). - std::erase_if(s_registered, [](const auto& w) { return !w.get(); }); - - // Register any TerminalPage we haven't seen before. If the page - // isn't ready yet (early startup race), skip silently — the next - // Subscribe() or s_OnWindowAdded() call will retry. - for (const auto& host : s_emperor->GetWindows()) - { - const auto page = _getPage(host.get()); - if (!page) - { - continue; - } - - const bool alreadyRegistered = std::any_of( - s_registered.begin(), - s_registered.end(), - [&page](const auto& w) { - const auto p = w.get(); - return p && p == page; - }); - if (alreadyRegistered) - { - continue; - } - - page.ProtocolVtSequenceReceived( - [](auto&&, const winrt::hstring& eventJson) { - s_NotifyEventToComClients(winrt::to_string(eventJson)); - }); - s_registered.push_back(winrt::make_weak(page)); - } -} - -void TerminalProtocolComServer::s_OnWindowAdded(AppHost* /*host*/) -{ - _ensurePageEventsRegistered(); -} - -void TerminalProtocolComServer::s_NotifyEventToComClients(const std::string& eventJson) -{ - wil::unique_bstr eventBstr{ ::SysAllocString(winrt::to_hstring(eventJson).c_str()) }; - if (!eventBstr) - { - // OOM allocating the event payload — skip this delivery rather than - // calling OnEvent with a null BSTR, which would break the documented - // JSON-string event contract. - return; - } - - // Snapshot the agile sink references under lock, then invoke outside the - // lock to avoid deadlocks if a callback reenters the server (e.g. via - // SendEvent). - std::vector> sinks; - { - std::lock_guard lock{ s_instancesMutex }; - for (auto* instance : s_instances) - { - std::lock_guard cbLock{ instance->_callbackMutex }; - if (instance->_sinkRef) - sinks.push_back(instance->_sinkRef); - } - } - - // TODO: event fan-out runs on the UI/STA thread (ProtocolVtSequenceReceived - // is raised there), and each OnEvent below is a SYNCHRONOUS cross-process - // call. A slow or blocked subscriber (e.g. wtcli's stdout pipe full because - // wta isn't draining it) therefore stalls the terminal UI thread, and the - // cost is serial across all subscribers. For high event volume this should - // be moved off the UI thread — queue events and drain them on a dedicated - // background MTA thread (mind ordering, back-pressure, and subscriber - // add/remove thread-safety), and/or give OnEvent a timeout. Tracked as - // follow-up; not addressed in the WinRT->classic-COM migration. - for (auto& ref : sinks) - { - // Resolve the agile reference to a sink proxy valid on THIS thread, - // then call it — this is the cross-apartment-safe callback path. - ComPtr sink; - if (FAILED(ref->Resolve(IID_PPV_ARGS(&sink))) || !sink) - continue; - - if (FAILED(sink->OnEvent(eventBstr.get()))) - { - // Client disconnected — clear the matching sink reference. - std::lock_guard lock{ s_instancesMutex }; - for (auto* instance : s_instances) - { - std::lock_guard cbLock{ instance->_callbackMutex }; - if (instance->_sinkRef.Get() == ref.Get()) - { - instance->_sinkRef.Reset(); - break; - } - } - } - } -} - // ============================================================================ // ITerminalProtocol — Meta // ============================================================================ @@ -875,112 +730,4 @@ try } CATCH_RETURN() -// ============================================================================ -// Events — push-based via classic sink -// ============================================================================ - -STDMETHODIMP TerminalProtocolComServer::Subscribe(ITerminalProtocolEventSink* sink) -try -{ - RETURN_HR_IF(E_INVALIDARG, !sink); - - // Store the sink as an agile reference so it can be resolved + called from - // any apartment (the VT-event fan-out runs on the UI/STA thread, while the - // sink was unmarshaled on an MTA thread). - ComPtr ref; - RETURN_IF_FAILED(::RoGetAgileReference(AGILEREFERENCE_DEFAULT, __uuidof(ITerminalProtocolEventSink), sink, &ref)); - - { - std::lock_guard lock{ _callbackMutex }; - _sinkRef = ref; - } - - // Ensure page events are wired up (one-time global init). - _ensurePageEventsRegistered(); - return S_OK; -} -CATCH_RETURN() - -STDMETHODIMP TerminalProtocolComServer::Unsubscribe() -{ - std::lock_guard lock{ _callbackMutex }; - _sinkRef.Reset(); - return S_OK; -} - -STDMETHODIMP TerminalProtocolComServer::SendEvent(BSTR eventJson) -try -{ - const auto eventH = _hstr(eventJson); - auto jsonStr = winrt::to_string(eventH); - Json::Value evt; - const auto route = ProtocolParsing::ClassifySendEvent(jsonStr, evt); - RETURN_HR_IF(E_INVALIDARG, route == ProtocolParsing::SendEventRoute::Invalid); - - switch (route) - { - case ProtocolParsing::SendEventRoute::AutofixState: - _dispatchAutofixStateToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::AgentStatus: - _dispatchAgentStatusToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::CloseAgentPane: - // User pressed Ctrl+C twice in the wta TUI. Marshal to the UI - // thread; the page-side handler resolves the tab via `tab_id` - // and tears down that tab's agent pane. - _dispatchCloseAgentPaneToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::AgentState: - // Per-tab agent-pane UI snapshot from wta. Page-side handler - // routes by `tab_id` to the matching AgentPaneContent (creating - // or tearing down the pane on that tab as needed). - _dispatchAgentStateChangedToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::ResumeInNewAgentTab: - // Session view's Shift+Enter handler in the wta TUI. WT creates - // a new tab and asks wta to open an agent pane in it. - _dispatchResumeInNewAgentTabToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::AgentChipTarget: - // Helper override for which pane gets the "Agent" chip; null - // pane_session_id reverts the tab to source-flag-driven chip. - _dispatchAgentChipTargetToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::RestartAgentStack: - // `/restart` from any agent pane TUI. Page-side handler tears - // down every agent pane in its window and force-restarts the - // wta-master process via SharedWta. - _dispatchRestartAgentStackToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::RestartAgentPane: - // Master detected a helper's pipe disconnect (crash or clean - // exit). Page-side handler resolves the tab via `tab_id` and - // re-warms a fresh helper, resuming `session_id`. Suppressed when - // the pane was torn down deliberately (Ctrl+C×2, tab close). - _dispatchRestartAgentPaneToPage(eventH); - return S_OK; - case ProtocolParsing::SendEventRoute::Broadcast: - { - Json::StreamWriterBuilder wb; - wb["indentation"] = ""; - s_NotifyEventToComClients(Json::writeString(wb, evt)); - return S_OK; - } - default: - return S_OK; - } -} -CATCH_RETURN() - -// Agent-pane dispatch methods — stubbed out (agent pane infrastructure not ported). -void TerminalProtocolComServer::_dispatchAutofixStateToPage(const winrt::hstring& /*eventJson*/) {} -void TerminalProtocolComServer::_dispatchAgentStatusToPage(const winrt::hstring& /*eventJson*/) {} -void TerminalProtocolComServer::_dispatchCloseAgentPaneToPage(const winrt::hstring& /*eventJson*/) {} -void TerminalProtocolComServer::_dispatchAgentStateChangedToPage(const winrt::hstring& /*eventJson*/) {} -void TerminalProtocolComServer::_dispatchResumeInNewAgentTabToPage(const winrt::hstring& /*eventJson*/) {} -void TerminalProtocolComServer::_dispatchAgentChipTargetToPage(const winrt::hstring& /*eventJson*/) {} -void TerminalProtocolComServer::_dispatchRestartAgentStackToPage(const winrt::hstring& /*eventJson*/) {} -void TerminalProtocolComServer::_dispatchRestartAgentPaneToPage(const winrt::hstring& /*eventJson*/) {} - diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h index 8e731e14e42..07845ee500c 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -3,16 +3,12 @@ #pragma once -#include -#include - #include #include #include "ITerminalProtocol.h" -// Per-brand CLSIDs — same pattern as CTerminalHandoff. Reused unchanged from the -// previous WinRT/MBM server, so WT_COM_CLSID discovery on the client is identical. +// 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) @@ -36,8 +32,6 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< Microsoft::WRL::RuntimeClassFlags, ITerminalProtocol> { - ~TerminalProtocolComServer(); - // ── ITerminalProtocol ── STDMETHODIMP GetCapabilities(BSTR* json) override; STDMETHODIMP GetActivePane(BSTR* json) override; @@ -54,9 +48,6 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< STDMETHODIMP SendInput(GUID sessionId, BSTR text) override; STDMETHODIMP FocusPane(GUID sessionId) override; STDMETHODIMP SetSessionVariable(GUID sessionId, BSTR name, BSTR value) override; - STDMETHODIMP Subscribe(ITerminalProtocolEventSink* sink) override; - STDMETHODIMP Unsubscribe() override; - STDMETHODIMP SendEvent(BSTR eventJson) override; // Static setup — must be called before s_StartListening(). static void s_setEmperor(WindowEmperor* emperor) noexcept; @@ -64,40 +55,7 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< static HRESULT s_StartListening(); static HRESULT s_StopListening(); - // Re-runs per-window page event registration after a new AppHost is added. - static void s_OnWindowAdded(class AppHost* host); - - // Deliver an event to all connected COM clients. - static void s_NotifyEventToComClients(const std::string& eventJson); - private: - // Per-instance event sink, stored as an agile reference so it can be - // resolved + called from any apartment (set via Subscribe, cleared via - // Unsubscribe). - std::mutex _callbackMutex; - Microsoft::WRL::ComPtr _sinkRef; - - // Static tracking of live COM instances for event delivery. - static std::mutex s_instancesMutex; - static std::vector s_instances; - - bool _instanceRegistered{ false }; - - void _addInstance(); - void _removeInstance(); - static void _ensurePageEventsRegistered(); - - // Per-method UI-thread dispatch helpers (unchanged from the WinRT server; - // they marshal SendEvent payloads onto each window's TerminalPage). - static void _dispatchAutofixStateToPage(const winrt::hstring& eventJson); - static void _dispatchAgentStatusToPage(const winrt::hstring& eventJson); - static void _dispatchCloseAgentPaneToPage(const winrt::hstring& eventJson); - static void _dispatchAgentStateChangedToPage(const winrt::hstring& eventJson); - static void _dispatchResumeInNewAgentTabToPage(const winrt::hstring& eventJson); - static void _dispatchAgentChipTargetToPage(const winrt::hstring& eventJson); - static void _dispatchRestartAgentStackToPage(const winrt::hstring& eventJson); - static void _dispatchRestartAgentPaneToPage(const winrt::hstring& eventJson); - static WindowEmperor* s_emperor; }; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 394d0d8bc14..2232d8067bc 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -284,11 +284,6 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args _windowCount += 1; _windows.emplace_back(std::move(host)); - // Wire the new window's TerminalPage::ProtocolVtSequenceReceived - // into the COM fan-out so events emitted by panes in this window - // actually reach connected wta clients. - TerminalProtocolComServer::s_OnWindowAdded(_windows.back().get()); - if (_windowCount == 1) { // The first CoreWindow is created implicitly by XAML and parented to the diff --git a/src/host/proxy/ITerminalProtocol.idl b/src/host/proxy/ITerminalProtocol.idl index c623b24de14..252747587a7 100644 --- a/src/host/proxy/ITerminalProtocol.idl +++ b/src/host/proxy/ITerminalProtocol.idl @@ -17,17 +17,6 @@ import "unknwn.idl"; // LOAD BEARING (same discipline as ITerminalHandoff): OpenConsoleProxy.dll is a // single shared proxy/stub DLL. Never change or reuse the UUIDs below. -// Event sink — implemented by the CLIENT (wtcli). The server calls back into -// this cross-process to push events (each event is a JSON string). -[ - object, - uuid(DD47E357-3D5E-4EFD-901B-C02CE6C45B7D), - pointer_default(unique) -] interface ITerminalProtocolEventSink : IUnknown -{ - HRESULT OnEvent([in] BSTR eventJson); -}; - // Terminal protocol — implemented by WindowsTerminal.exe, activated by the // client via CoCreateInstance(CLSCTX_LOCAL_SERVER) with the per-brand CLSID. [ @@ -69,9 +58,4 @@ import "unknwn.idl"; HRESULT SendInput([in] GUID sessionId, [in] BSTR text); HRESULT FocusPane([in] GUID sessionId); HRESULT SetSessionVariable([in] GUID sessionId, [in] BSTR name, [in] BSTR value); - - // ── Events ── - HRESULT Subscribe([in] ITerminalProtocolEventSink* sink); - HRESULT Unsubscribe(); - HRESULT SendEvent([in] BSTR eventJson); }; diff --git a/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp b/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp index 63b47f751e0..3e86535f580 100644 --- a/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp +++ b/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp @@ -2,7 +2,6 @@ // Licensed under the MIT license. // // Fuzzing harness for wtcli CLI utility functions. -// Targets: BuildSendEventJson, MatchesEventFilter. // // Built under the Fuzzing MSBuild configuration with LibFuzzer // instrumentation; submittable to OneFuzz via the CI pipeline. @@ -10,30 +9,6 @@ #include "precomp.h" #include "wtcli_functions.h" -// Split fuzz input into N segments using null bytes as delimiters. -// If fewer than N null bytes exist, remaining segments are empty. -static std::vector SplitInput(const uint8_t* data, size_t size, size_t n) -{ - std::vector segments(n); - size_t seg = 0; - size_t start = 0; - for (size_t i = 0; i < size && seg < n - 1; ++i) - { - if (data[i] == '\0') - { - segments[seg].assign(reinterpret_cast(data + start), i - start); - start = i + 1; - ++seg; - } - } - // Remainder goes into the last segment. - if (start < size) - { - segments[seg].assign(reinterpret_cast(data + start), size - start); - } - return segments; -} - // Core fuzzing logic — called by both LibFuzzer and the manual main(). static int FuzzOneInput(const uint8_t* data, size_t size) { @@ -42,46 +17,9 @@ static int FuzzOneInput(const uint8_t* data, size_t size) return 0; } - // Split the entire input into segments for use across all targets. - // We need at least 4 segments: [eventType] [paramsJson] - // [sessionId] [eventTypeFilter] - auto parts = SplitInput(data, size, 4); - - // ── Target 1: BuildSendEventJson ── - // Fuzz all three input parameters: eventType, paramsJson, and sessionId. - { - Json::Value evt; - wtcli::BuildSendEventJson(parts[0], parts[1], parts[2], evt); - } - - // ── Target 2: MatchesEventFilter ── - // Construct valid JSON from fuzzed fields using Json::Value so the parser - // succeeds and the deep matching logic (session_id, wildcard) is reached - // even when fuzzed strings contain quotes/backslashes/control chars. - { - // 2a: Fuzzed event structure with fuzzed filter. - const auto& fuzzedSessionId = parts[2]; - const auto& fuzzedEvent = parts[0]; - const auto& fuzzedFilter = parts[3]; - - Json::Value params; - params["session_id"] = fuzzedSessionId; - params["event"] = fuzzedEvent; - Json::Value ev; - ev["params"] = params; - Json::StreamWriterBuilder wb; - wb["indentation"] = ""; - auto syntheticJson = Json::writeString(wb, ev); - - wtcli::MatchesEventFilter(syntheticJson, fuzzedSessionId, fuzzedFilter); - - // 2b: Mismatched session_id — exercises the rejection path. - wtcli::MatchesEventFilter(syntheticJson, "999", fuzzedFilter); - - // 2c: Raw fuzzed bytes as event JSON — exercises parse-failure path. - const std::string raw(reinterpret_cast(data), size); - wtcli::MatchesEventFilter(raw, fuzzedSessionId, fuzzedFilter); - } + // Fuzz ExpandEnvironmentStringsInUtf8 + const std::string input(reinterpret_cast(data), size); + (void)wtcli::ExpandEnvironmentStringsInUtf8(input); return 0; } diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index b95f8d62713..849b0e1df42 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -28,44 +28,6 @@ #include #include -// ── EventSink — pure classic-COM event sink for `listen` ── -struct EventSink : ITerminalProtocolEventSink -{ - LONG _ref{ 1 }; - std::function _handler; - - explicit EventSink(std::function handler) : - _handler(std::move(handler)) {} - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override - { - if (!ppv) - return E_POINTER; - if (riid == __uuidof(IUnknown) || riid == __uuidof(ITerminalProtocolEventSink)) - { - *ppv = static_cast(this); - AddRef(); - return S_OK; - } - *ppv = nullptr; - return E_NOINTERFACE; - } - ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&_ref); } - ULONG STDMETHODCALLTYPE Release() override - { - const auto r = InterlockedDecrement(&_ref); - if (r == 0) - delete this; - return r; - } - HRESULT STDMETHODCALLTYPE OnEvent(BSTR eventJson) override - { - if (_handler) - _handler(eventJson ? winrt::to_string(winrt::hstring{ eventJson }) : std::string{}); - return S_OK; - } -}; - // ── Helpers ── // Per-brand CLSID — matches the server's compile-time selection. @@ -709,114 +671,6 @@ int main() } }); - // ── publish ── - // Low-level "pass this JSON through to SendEvent verbatim" escape hatch. - std::string publishJson; - auto* publishCmd = app.add_subcommand("publish", "Forward raw JSON to SendEvent"); - publishCmd->add_option("json", publishJson, "Full event JSON (e.g. {\"method\":\"autofix_state\",\"params\":{...}})")->required(); - publishCmd->callback([&]() { - auto server = connect(); - if (!server) return; - wil::unique_bstr evt{ Bstr(publishJson) }; - auto hr = server->SendEvent(evt.get()); - if (FAILED(hr)) { fprintf(stderr, "publish failed: 0x%08X\n", static_cast(hr)); exitCode = 1; } - }); - - // ── send-event ── - std::string sendEventType, sendEventJson, sendEventPaneTarget; - auto* sendEventCmd = app.add_subcommand("send-event", "Publish an event to all listeners")->alias("se"); - sendEventCmd->add_option("-p,--pane", sendEventPaneTarget, "Source session ID (GUID)"); - sendEventCmd->add_option("-e,--event", sendEventType, "Event type (e.g. agent.task.started)")->required(); - sendEventCmd->add_option("json", sendEventJson, "Event params as JSON object"); - sendEventCmd->callback([&]() { - auto server = connect(); - if (!server) return; - std::string resolvedSessionId; - if (!sendEventPaneTarget.empty()) - { - resolvedSessionId = sendEventPaneTarget; - } - else - { - // Fall back to the active pane as the event source. If there is no - // active pane, bail rather than sending with an all-zero GUID, - // which would silently misroute the event. - const auto activeSid = ResolveSessionId(server.get(), ""); - if (IsEqualGUID(activeSid, GUID{})) - { - fprintf(stderr, "[wtcli] send-event: no --pane given and no active pane to use as the event source.\n"); - exitCode = 1; - return; - } - resolvedSessionId = GuidToString(activeSid); - } - Json::Value evt; - if (!wtcli::BuildSendEventJson(sendEventType, sendEventJson, resolvedSessionId, evt)) - { - fprintf(stderr, "Invalid JSON for --json: value must be a JSON object (e.g. '{\"key\":\"val\"}')\n"); - exitCode = 1; - return; - } - Json::StreamWriterBuilder wb; - wb["indentation"] = ""; - wil::unique_bstr evtB{ Bstr(Json::writeString(wb, evt)) }; - auto hr = server->SendEvent(evtB.get()); - if (FAILED(hr)) { fprintf(stderr, "SendEvent failed: 0x%08X\n", static_cast(hr)); exitCode = 1; } - }); - - // ── listen ── - std::string listenTarget; - std::string listenEventFilter; - auto* listenCmd = app.add_subcommand("listen", "Stream real-time events from Windows Terminal"); - listenCmd->add_option("-t,--target", listenTarget, "Filter by session ID (GUID)"); - listenCmd->add_option("--event", listenEventFilter, "Filter by event type (supports trailing wildcard, e.g. agent.*)"); - listenCmd->callback([&]() { - auto server = connect(); - if (!server) { exitCode = 1; return; } - - static HANDLE s_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (!s_stopEvent) - { - fprintf(stderr, "[wtcli] listen: failed to create stop event (0x%08X)\n", GetLastError()); - exitCode = 1; - return; - } - SetConsoleCtrlHandler([](DWORD) -> BOOL { - SetEvent(s_stopEvent); - return TRUE; - }, TRUE); - - if (!jsonMode) - fprintf(stderr, "Listening for events... (Ctrl-C to stop)\n"); - - // EventSink is born with _ref == 1, so attach() (adopt, no AddRef) hands - // that reference to the com_ptr. RAII then Releases on every exit path -- - // exception-safe and robust against future early-returns, no manual - // Release to forget. - winrt::com_ptr sink; - sink.attach(new EventSink([&](const std::string& eventUtf8) { - if (!wtcli::MatchesEventFilter(eventUtf8, listenTarget, listenEventFilter)) - return; - printf("%s\n", eventUtf8.c_str()); - fflush(stdout); - })); - - auto hr = server->Subscribe(sink.get()); - if (FAILED(hr)) - { - fprintf(stderr, "Subscribe failed: 0x%08X\n", static_cast(hr)); - exitCode = 1; - return; - } - - WaitForSingleObject(s_stopEvent, INFINITE); - server->Unsubscribe(); - // s_stopEvent is intentionally NOT closed: it is static and still - // referenced by the registered Ctrl-C handler (a non-capturing lambda - // that can only reach it via the static), so closing it would leave the - // handler pointing at an invalid handle. It is reclaimed at process exit. - }); - // ── Default (no subcommand) ── app.callback([&]() { if (app.get_subcommands().empty()) diff --git a/src/tools/wtcli/wtcli_functions.h b/src/tools/wtcli/wtcli_functions.h index 3a5a6c242e8..ef8007da46b 100644 --- a/src/tools/wtcli/wtcli_functions.h +++ b/src/tools/wtcli/wtcli_functions.h @@ -94,122 +94,4 @@ namespace wtcli return result; } - - // Build the standard JSON envelope the COM server expects for an - // `agent_event`. The caller provides the event name, an optional JSON - // object string containing extra params, and the source pane Guid; this - // function folds in `pane_id` and emits the wrapped - // `{ type, method, params }` object in `outEvt`. - // - // Returns true on success and populates `outEvt`. - // Returns false and leaves `outEvt` untouched if `paramsJson` is - // non-empty but not a valid JSON object. - // - // |eventType| — required event name (e.g. "agent.task.started") - // |paramsJson| — optional JSON object string with extra params - // |sessionId| — source pane Guid as a string (already resolved by caller). - // Named `sessionId` for backwards compatibility with the - // old per-pane "session_id" terminology; the value is - // the WT pane GUID, which goes into `params["pane_id"]` - // — matching the rename in TerminalPage.cpp for - // connection_state / vt_sequence events. - inline bool BuildSendEventJson( - const std::string& eventType, - const std::string& paramsJson, - const std::string& sessionId, - Json::Value& outEvt) - { - Json::Value params; - if (!paramsJson.empty()) - { - Json::CharReaderBuilder rb; - std::string errs; - std::istringstream ss(paramsJson); - if (!Json::parseFromStream(rb, ss, ¶ms, &errs) || !params.isObject()) - { - return false; - } - } - - params["event"] = eventType; - params["pane_id"] = sessionId; - - outEvt["type"] = "event"; - outEvt["method"] = "agent_event"; - outEvt["params"] = params; - return true; - } - - // Check whether an event JSON string passes the session_id and event type - // filters used by the "listen" command. - // - // Returns true if the event should be emitted (matches filters or filters - // are empty). Returns true on parse failure to match original behavior - // (unparseable events are passed through). - // - // |eventTypeFilter| supports a trailing wildcard: "agent.*" matches - // "agent.task.started". - inline bool MatchesEventFilter( - const std::string& eventJson, - const std::string& sessionIdFilter, - const std::string& eventTypeFilter) - { - if (sessionIdFilter.empty() && eventTypeFilter.empty()) - { - return true; - } - - Json::Value ev; - Json::CharReaderBuilder rb; - std::string errs; - std::istringstream ss(eventJson); - if (!Json::parseFromStream(rb, ss, &ev, &errs)) - { - return true; - } - - // Event JSON must be an object with a "params" object inside. - // Reject structurally invalid events when filters are active — - // missing fields can't match any filter. - if (!ev.isObject() || !ev.isMember("params") || !ev["params"].isObject()) - { - return false; - } - - if (!sessionIdFilter.empty()) - { - // Look for pane_id (current name) first, then fall back to - // session_id (old name) so older listen consumers / events - // produced before the rename keep matching during a - // partial upgrade. - auto paneId = ev["params"].get("pane_id", "").asString(); - if (paneId.empty()) - { - paneId = ev["params"].get("session_id", "").asString(); - } - if (paneId != sessionIdFilter) - { - return false; - } - } - - if (!eventTypeFilter.empty()) - { - auto eventType = ev["params"].get("event", "").asString(); - if (eventTypeFilter.back() == '*') - { - auto prefix = eventTypeFilter.substr(0, eventTypeFilter.size() - 1); - if (eventType.substr(0, prefix.size()) != prefix) - { - return false; - } - } - else if (eventType != eventTypeFilter) - { - return false; - } - } - - return true; - } } From 3096e1d285a694fdd80fa9ffd55eef4697f717b6 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Tue, 7 Jul 2026 15:46:23 -0700 Subject: [PATCH 07/26] cleanup fuzzer --- .../TerminalProtocol/ProtocolParsing.h | 105 ------------------ .../TerminalProtocol/ft_fuzzer/fuzzmain.cpp | 15 +-- 2 files changed, 3 insertions(+), 117 deletions(-) diff --git a/src/cascadia/TerminalProtocol/ProtocolParsing.h b/src/cascadia/TerminalProtocol/ProtocolParsing.h index ae5f307d00d..6d6ee48b215 100644 --- a/src/cascadia/TerminalProtocol/ProtocolParsing.h +++ b/src/cascadia/TerminalProtocol/ProtocolParsing.h @@ -8,114 +8,9 @@ #pragma once #include -#include - -#include namespace Microsoft::Terminal::Protocol::Parsing { - // ── JSON helper ── - - // Parse a JSON string. Returns true on success. - // Equivalent to _parseJson() in TerminalProtocolComServer.cpp. - inline 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); - } - - // ── SendEvent dispatch ── - - // The dispatch routes for IProtocolServer::SendEvent. - enum class SendEventRoute - { - AutofixState, // Direct to TerminalPage, no broadcast - AgentStatus, // Direct to TerminalPage, no broadcast - CloseAgentPane, // Direct to TerminalPage, no broadcast - AgentState, // Direct to TerminalPage, no broadcast — unified per-tab agent-pane UI snapshot (view + pane_open + ...) - ResumeInNewAgentTab, // Direct to TerminalPage, no broadcast - AgentChipTarget, // Direct to TerminalPage, no broadcast — "draw the Agent chip on this pane (or hide override)" - RestartAgentStack, // Direct to TerminalPage, no broadcast — `/restart` from any agent pane TUI - RestartAgentPane, // Direct to TerminalPage, no broadcast — master detected helper death; re-warm a fresh helper for this tab - Broadcast, // Normalize envelope + broadcast to all subscribers - Invalid // Failed validation - }; - - // Classify and validate a SendEvent JSON payload. - // - // On success, |outEvt| contains the parsed JSON. For the Broadcast route - // the envelope is normalized (type=event, method=agent_event). - // - // Returns Invalid when: - // - JSON parsing fails - // - The broadcast path is selected but params.event is missing - inline SendEventRoute ClassifySendEvent(const std::string& eventJson, Json::Value& outEvt) - { - if (!ParseJson(eventJson, outEvt)) - { - return SendEventRoute::Invalid; - } - - // Event JSON must be an object to inspect fields. - if (!outEvt.isObject()) - { - return SendEventRoute::Invalid; - } - - // Check method-based direct dispatch routes - if (outEvt.isMember("method") && outEvt["method"].isString()) - { - const auto method = outEvt["method"].asString(); - if (method == "autofix_state") - { - return SendEventRoute::AutofixState; - } - if (method == "agent_status") - { - return SendEventRoute::AgentStatus; - } - if (method == "close_agent_pane") - { - return SendEventRoute::CloseAgentPane; - } - if (method == "agent_state_changed") - { - return SendEventRoute::AgentState; - } - if (method == "resume_in_new_agent_tab") - { - return SendEventRoute::ResumeInNewAgentTab; - } - if (method == "set_agent_chip_target") - { - return SendEventRoute::AgentChipTarget; - } - if (method == "restart_agent_stack") - { - return SendEventRoute::RestartAgentStack; - } - if (method == "restart_agent_pane") - { - return SendEventRoute::RestartAgentPane; - } - } - - // Broadcast path: params.event is required - if (!outEvt.isMember("params") || !outEvt["params"].isObject() || - !outEvt["params"].isMember("event")) - { - return SendEventRoute::Invalid; - } - - // Normalize the envelope - outEvt["type"] = "event"; - outEvt["method"] = "agent_event"; - - return SendEventRoute::Broadcast; - } - // ── SplitPane direction mapping ── // Mirror of TerminalSettingsModel::SplitDirection enum values. diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp index a2619c51b11..e84b561e90c 100644 --- a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp +++ b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT license. // // Fuzzing harness for Terminal Protocol server parsing logic. -// Targets: ClassifySendEvent, ParseSplitDirection, ClassifyPaneOutputSource. +// Targets: ParseSplitDirection, ClassifyPaneOutputSource. // // Built under the Fuzzing MSBuild configuration with LibFuzzer // instrumentation; submittable to OneFuzz via the CI pipeline. @@ -22,23 +22,14 @@ static int FuzzOneInput(const uint8_t* data, size_t size) const std::string input(reinterpret_cast(data), size); - // ── Target 1: ClassifySendEvent ── - // Feed fuzzed data as a SendEvent JSON payload. Exercises JSON parsing, - // method field inspection, and params.event validation across all 3 - // dispatch routes (AutofixState, AgentStatus, Broadcast). - { - Json::Value evt; - ProtocolParsing::ClassifySendEvent(input, evt); - } - - // ── Target 2: ParseSplitDirection ── + // ── Target 1: ParseSplitDirection ── // Feed fuzzed data as a direction string. Exercises all 8 recognized // values plus arbitrary unrecognized strings. { ProtocolParsing::ParseSplitDirection(input); } - // ── Target 3: ClassifyPaneOutputSource ── + // ── Target 2: ClassifyPaneOutputSource ── // Feed fuzzed data as a source parameter. { ProtocolParsing::ClassifyPaneOutputSource(input); From 3c762e4fb389a6e56d223ab9a22d2cb87469d25e Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Thu, 9 Jul 2026 13:32:58 -0700 Subject: [PATCH 08/26] okay it works here --- OpenConsole.slnx | 9 - .../ft_fuzzer/WtcliFuzzer-OneFuzzConfig.json | 39 ---- src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj | 56 ----- src/tools/wtcli/ft_fuzzer/fuzzmain.cpp | 49 ----- src/tools/wtcli/ft_fuzzer/precomp.cpp | 4 - src/tools/wtcli/ft_fuzzer/precomp.h | 17 -- src/tools/wtcli/main.cpp | 201 ------------------ src/tools/wtcli/wtcli.vcxproj | 1 - src/tools/wtcli/wtcli_functions.h | 97 --------- 9 files changed, 473 deletions(-) delete mode 100644 src/tools/wtcli/ft_fuzzer/WtcliFuzzer-OneFuzzConfig.json delete mode 100644 src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj delete mode 100644 src/tools/wtcli/ft_fuzzer/fuzzmain.cpp delete mode 100644 src/tools/wtcli/ft_fuzzer/precomp.cpp delete mode 100644 src/tools/wtcli/ft_fuzzer/precomp.h delete mode 100644 src/tools/wtcli/wtcli_functions.h diff --git a/OpenConsole.slnx b/OpenConsole.slnx index 20a200c7259..6482d3d340d 100644 --- a/OpenConsole.slnx +++ b/OpenConsole.slnx @@ -1048,15 +1048,6 @@ - - - - - - - - - diff --git a/src/tools/wtcli/ft_fuzzer/WtcliFuzzer-OneFuzzConfig.json b/src/tools/wtcli/ft_fuzzer/WtcliFuzzer-OneFuzzConfig.json deleted file mode 100644 index 37d68f0acd6..00000000000 --- a/src/tools/wtcli/ft_fuzzer/WtcliFuzzer-OneFuzzConfig.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "configVersion": 3, - "entries": [ - { - "Fuzzer": { - "$type": "libfuzzer", - "FuzzingHarnessExecutableName": "WtcliFuzzer.exe", - "FuzzingTargetBinaries": [ - "WtcliFuzzer.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.Wtcli", - "targetName": "Wtcli_Fuzzer" - } - ], - "jobDependencies": [ - "WtcliFuzzer.exe", - "WtcliFuzzer.pdb", - "WtcliFuzzer.lib", - "clang_rt.asan_dynamic-x86_64.dll" - ], - "SdlWorkItemId": 62007365, - "SdlWorkItemProjectUrl": "https://dev.azure.com/microsoft/OS" - } - ] -} diff --git a/src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj b/src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj deleted file mode 100644 index fde693ed725..00000000000 --- a/src/tools/wtcli/ft_fuzzer/WtcliFuzzer.vcxproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - {C5FAE84E-310E-4CBC-8242-5AD6A37FDAA5} - Win32Proj - WtcliFuzzer - Wtcli.Fuzzer - WtcliFuzzer - Application - - - - - - - - - - Create - ProgramDatabase - - - - - - ..;%(AdditionalIncludeDirectories) - - - Console - - - - true - true - - - - - _DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION;%(PreprocessorDefinitions) - - - clang_rt.fuzzer_MT-$(OCClangArchitectureName).lib;%(AdditionalDependencies) - - - xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)" - Copy the required ASan runtime DLL to the output directory. - - - - - - - - - diff --git a/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp b/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp deleted file mode 100644 index 3e86535f580..00000000000 --- a/src/tools/wtcli/ft_fuzzer/fuzzmain.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -// Fuzzing harness for wtcli CLI utility functions. -// -// Built under the Fuzzing MSBuild configuration with LibFuzzer -// instrumentation; submittable to OneFuzz via the CI pipeline. - -#include "precomp.h" -#include "wtcli_functions.h" - -// 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; - } - - // Fuzz ExpandEnvironmentStringsInUtf8 - const std::string input(reinterpret_cast(data), size); - (void)wtcli::ExpandEnvironmentStringsInUtf8(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: WtcliFuzzer \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/tools/wtcli/ft_fuzzer/precomp.cpp b/src/tools/wtcli/ft_fuzzer/precomp.cpp deleted file mode 100644 index c51e9b31b2f..00000000000 --- a/src/tools/wtcli/ft_fuzzer/precomp.cpp +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" diff --git a/src/tools/wtcli/ft_fuzzer/precomp.h b/src/tools/wtcli/ft_fuzzer/precomp.h deleted file mode 100644 index 768ce96863e..00000000000 --- a/src/tools/wtcli/ft_fuzzer/precomp.h +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#define NOMINMAX - -#include - -#include -#include -#include -#include -#include -#include - -#include diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 849b0e1df42..5ebc29b65b2 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -5,7 +5,6 @@ #include #include "Formatting.h" -#include "wtcli_functions.h" // Classic-COM Terminal protocol. Generated from // src/host/proxy/ITerminalProtocol.idl; found via the OpenConsoleProxy IntDir @@ -19,13 +18,10 @@ #include #include -#include #include #include -#include #include #include -#include #include // ── Helpers ── @@ -451,36 +447,6 @@ int main() } }); - // ── send-keys ── - std::string sendKeysTarget; - std::vector sendKeysArgs; - bool sendKeysRaw = false; - auto* sendKeysCmd = app.add_subcommand("send-keys", "Send keys to a pane")->alias("send"); - sendKeysCmd->add_option("-t,--target", sendKeysTarget, "Session ID (GUID)"); - sendKeysCmd->add_flag("--raw", sendKeysRaw, - "Treat the payload as literal UTF-8 text — skip tmux-style " - "token translation (Enter/Tab/Escape/BSpace/C-x). Use this when " - "forwarding arbitrary agent-supplied text."); - sendKeysCmd->add_option("keys", sendKeysArgs, "Keys to send")->required(); - sendKeysCmd->callback([&]() { - auto server = connect(); - if (!server) return; - auto sessionId = ResolveSessionId(server.get(), sendKeysTarget); - auto text = sendKeysRaw - ? wtcli::JoinAsUtf16(sendKeysArgs) - : wtcli::TranslateKeys(sendKeysArgs); - wil::unique_bstr textB{ SysAllocString(text.c_str()) }; - auto hr = server->SendInput(sessionId, textB.get()); - if (FAILED(hr)) { fprintf(stderr, "SendInput failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } - if (jsonMode) - { - Json::Value v; - v["ok"] = true; - v["session_id"] = GuidToString(sessionId); - PrintJson(v); - } - }); - // ── focus-pane ── std::string focusPaneTarget; auto* focusPaneCmd = app.add_subcommand("focus-pane", "Switch focus to a pane")->alias("focusp"); @@ -504,173 +470,6 @@ int main() } }); - // ── test-pipe ── - auto* testPipeCmd = app.add_subcommand("test-pipe", "Test connection to Windows Terminal"); - testPipeCmd->callback([&]() { - printf("Connecting to Windows Terminal...\n"); - auto server = connect(); - if (!server) { fprintf(stderr, "Connection failed.\n"); return; } - printf("Connected!\n\n"); - - Json::Value windows; - if (SUCCEEDED(CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows))) - { - Json::Value arr(Json::objectValue); - arr["windows"] = windows; - printf("list_windows:\n"); - PrintJson(arr); - } - printf("\n"); - - Json::Value caps; - if (SUCCEEDED(CallJson([&](BSTR* j) { return server->GetCapabilities(j); }, caps))) - { - printf("get_capabilities:\n"); - PrintJson(caps); - } - }); - - // ── info ── - auto* infoCmd = app.add_subcommand("info", "Show connection info"); - infoCmd->callback([&]() { - wchar_t clsid[128]{}; - auto hasClsid = GetEnvironmentVariableW(L"WT_COM_CLSID", clsid, ARRAYSIZE(clsid)) > 0; - - std::string version; - auto server = ConnectToTerminal(&version); - - Json::Value methods(Json::arrayValue); - if (server) - { - CallJson([&](BSTR* j) { return server->GetCapabilities(j); }, methods); - } - - if (jsonMode) - { - Json::Value v; - if (hasClsid) - v["com_clsid"] = winrt::to_string(winrt::hstring{ clsid }); - v["connected"] = (server != nullptr); - if (!version.empty()) - v["protocol_version"] = version; - v["methods"] = methods.isArray() ? methods : Json::Value(Json::arrayValue); - PrintJson(v); - } - else - { - printf("Windows Terminal Protocol Info\n"); - printf("========================================\n"); - if (hasClsid) - printf(" COM CLSID: %ls\n", clsid); - else - printf(" COM CLSID: (not set)\n"); - printf("\n"); - if (!server) - { - printf(" Connection: FAILED\n"); - } - else - { - printf(" Connection: OK\n"); - if (!version.empty()) - printf(" Protocol: %s\n", version.c_str()); - printf("\n"); - if (methods.isArray() && methods.size() > 0) - { - printf(" Methods: %u supported\n", methods.size()); - for (const auto& m : methods) - printf(" - %s\n", m.asString().c_str()); - } - } - } - - if (!server) - exitCode = 1; - }); - - // ── wait-for ── - std::string waitForTarget; - int waitInterval = 500; - int waitTimeout = 0; - auto* waitForCmd = app.add_subcommand("wait-for", "Wait for a pane to exit"); - waitForCmd->add_option("-t,--target", waitForTarget, "Session ID (GUID)")->required(); - waitForCmd->add_option("--interval", waitInterval, "Poll interval (ms)"); - waitForCmd->add_option("--timeout", waitTimeout, "Timeout (seconds, 0=forever)"); - waitForCmd->callback([&]() { - auto server = connect(); - if (!server) return; - auto sessionId = ResolveSessionId(server.get(), waitForTarget); - auto start = std::chrono::steady_clock::now(); - - while (true) - { - Json::Value status; - auto hr = CallJson([&](BSTR* j) { return server->GetProcessStatus(sessionId, j); }, status); - if (FAILED(hr)) - { - fprintf(stderr, "GetProcessStatus failed: 0x%08X\n", static_cast(hr)); - exitCode = 1; - return; - } - if (status["state"].asString() == "exited") - { - if (jsonMode) - { - Json::Value v; - v["state"] = "exited"; - if (status.isMember("exit_code")) - v["exit_code"] = status["exit_code"].asInt(); - PrintJson(v); - } - else - { - printf("Process exited"); - if (status.isMember("has_exit_code") ? status["has_exit_code"].asBool() : status.isMember("exit_code")) - printf(" (code %d)", status["exit_code"].asInt()); - printf("\n"); - } - return; - } - - if (waitTimeout > 0) - { - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - start) - .count(); - if (elapsed >= waitTimeout) - { - fprintf(stderr, "Timeout waiting for pane %s\n", waitForTarget.c_str()); - exitCode = 1; - return; - } - } - std::this_thread::sleep_for(std::chrono::milliseconds(waitInterval)); - } - }); - - // ── set-env ── - std::string setEnvShell = "powershell"; - auto* setEnvCmd = app.add_subcommand("set-env", "Print env setup commands")->alias("setenv"); - setEnvCmd->add_option("-s,--shell", setEnvShell, "Shell: powershell, bash, cmd"); - setEnvCmd->callback([&]() { - wchar_t clsid[128]{}; - GetEnvironmentVariableW(L"WT_COM_CLSID", clsid, ARRAYSIZE(clsid)); - auto cl = winrt::to_string(winrt::hstring{ clsid }); - - if (setEnvShell == "powershell" || setEnvShell == "pwsh") - { - if (!cl.empty()) printf("$env:WT_COM_CLSID = '%s'\n", cl.c_str()); - } - else if (setEnvShell == "bash" || setEnvShell == "sh" || setEnvShell == "zsh") - { - if (!cl.empty()) printf("export WT_COM_CLSID='%s'\n", cl.c_str()); - } - else if (setEnvShell == "cmd") - { - if (!cl.empty()) printf("set WT_COM_CLSID=%s\n", cl.c_str()); - } - }); - // ── Default (no subcommand) ── app.callback([&]() { if (app.get_subcommands().empty()) diff --git a/src/tools/wtcli/wtcli.vcxproj b/src/tools/wtcli/wtcli.vcxproj index 97eb143381a..42614cb82ff 100644 --- a/src/tools/wtcli/wtcli.vcxproj +++ b/src/tools/wtcli/wtcli.vcxproj @@ -21,7 +21,6 @@ - diff --git a/src/tools/wtcli/wtcli_functions.h b/src/tools/wtcli/wtcli_functions.h deleted file mode 100644 index ef8007da46b..00000000000 --- a/src/tools/wtcli/wtcli_functions.h +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -// Extracted pure functions from wtcli for fuzzing and testability. -// These functions have no COM/WinRT dependencies and can be called -// from a LibFuzzer harness. - -#pragma once - -#include -#include -#include - -#include -#include - -namespace wtcli -{ - // Concatenate positional args as literal UTF-8 → UTF-16 text without - // any tmux-style token interpretation. Use this when the caller's intent - // is "send these exact characters" (e.g. wta forwarding agent-supplied - // text), so payloads like the literal word "Enter" / "Tab" / "C-c" are - // not silently rewritten into control bytes. - inline std::wstring JoinAsUtf16(const std::vector& parts) - { - std::wstring result; - bool first = true; - for (const auto& p : parts) - { - // Space-separate consecutive args so an unquoted human invocation - // like `wtcli send-keys --raw hello world` reaches the pane as - // "hello world" rather than "helloworld". wta callers pass a - // single positional via `--`, so they are unaffected. - if (!first) - { - result += L' '; - } - first = false; - if (p.empty()) - continue; - const int wlen = MultiByteToWideChar(CP_UTF8, 0, p.data(), static_cast(p.size()), nullptr, 0); - if (wlen > 0) - { - const size_t prev = result.size(); - result.resize(prev + static_cast(wlen)); - MultiByteToWideChar(CP_UTF8, 0, p.data(), static_cast(p.size()), result.data() + prev, wlen); - } - } - return result; - } - - // Translate tmux-style key names to the byte stream that should be sent - // to a pane. Recognized tokens: Enter / Space / Tab / Escape (alias Esc) / - // BSpace / C-a..C-z. Unrecognized tokens are passed through as UTF-8 → - // UTF-16 text. "Enter" maps to a single CR — SendProtocolInput downstream - // translates LF to CR as well, so emitting CRLF here would produce a - // double-CR (two Enter keypresses). - inline std::wstring TranslateKeys(const std::vector& keys) - { - std::wstring result; - for (const auto& key : keys) - { - if (key == "Enter" || key == "enter") - result += L"\r"; - else if (key == "Space" || key == "space") - result += L" "; - else if (key == "Tab" || key == "tab") - result += L"\t"; - else if (key == "Escape" || key == "escape" || key == "Esc" || key == "esc") - result += L"\x1b"; - else if (key == "BSpace" || key == "bspace") - result += L"\b"; - else if (key == "C-c") - result += L"\x03"; - else if (key == "C-d") - result += L"\x04"; - else if (key == "C-z") - result += L"\x1a"; - else if (key == "C-l") - result += L"\x0c"; - else if (key.size() == 3 && key[0] == 'C' && key[1] == '-' && key[2] >= 'a' && key[2] <= 'z') - result += static_cast(key[2] - 'a' + 1); - else if (!key.empty()) - { - const int wlen = MultiByteToWideChar(CP_UTF8, 0, key.data(), static_cast(key.size()), nullptr, 0); - if (wlen > 0) - { - const size_t prev = result.size(); - result.resize(prev + static_cast(wlen)); - MultiByteToWideChar(CP_UTF8, 0, key.data(), static_cast(key.size()), result.data() + prev, wlen); - } - } - } - return result; - } - -} From e26675d578e64b2b437113c9713df89ec539f032 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Mon, 13 Jul 2026 16:06:44 -0700 Subject: [PATCH 09/26] remove get capabilities --- .../TerminalProtocolComServer.cpp | 41 ------------------- .../TerminalProtocolComServer.h | 1 - src/host/proxy/ITerminalProtocol.idl | 3 -- 3 files changed, 45 deletions(-) diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index b1b76be1b8d..b28f1116049 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -257,47 +257,6 @@ static winrt::hstring _hstr(BSTR b) return b ? winrt::hstring{ b } : winrt::hstring{}; } -// ============================================================================ -// ITerminalProtocol — Meta -// ============================================================================ - -STDMETHODIMP TerminalProtocolComServer::GetCapabilities(BSTR* json) -try -{ - RETURN_HR_IF_NULL(E_POINTER, json); - *json = nullptr; - - static const std::vector supportedMethods = { - "authenticate", - "get_capabilities", - "get_active_pane", - "list_windows", - "list_tabs", - "list_panes", - "read_pane_output", - "get_process_status", - "get_session_variable", - "get_settings", - "create_tab", - "split_pane", - "close_pane", - "send_input", - "focus_pane", - "set_session_variable", - "subscribe", - "unsubscribe", - "send_event", - }; - - Json::Value methods(Json::arrayValue); - for (const auto& m : supportedMethods) - methods.append(m); - - *json = _bstrFromJson(methods); - return S_OK; -} -CATCH_RETURN() - // ============================================================================ // Queries // ============================================================================ diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h index 07845ee500c..186f9979c5f 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -33,7 +33,6 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< ITerminalProtocol> { // ── ITerminalProtocol ── - STDMETHODIMP GetCapabilities(BSTR* json) override; STDMETHODIMP GetActivePane(BSTR* json) override; STDMETHODIMP ListWindows(BSTR* json) override; STDMETHODIMP ListTabs(unsigned __int64 windowIdFilter, BSTR* json) override; diff --git a/src/host/proxy/ITerminalProtocol.idl b/src/host/proxy/ITerminalProtocol.idl index 252747587a7..6070d5fca28 100644 --- a/src/host/proxy/ITerminalProtocol.idl +++ b/src/host/proxy/ITerminalProtocol.idl @@ -25,9 +25,6 @@ import "unknwn.idl"; pointer_default(unique) ] interface ITerminalProtocol : IUnknown { - // ── Meta ── - HRESULT GetCapabilities([out, retval] BSTR* json); - // ── Queries (complex results as JSON) ── HRESULT GetActivePane([out, retval] BSTR* json); HRESULT ListWindows([out, retval] BSTR* json); From 6c4a0354bcaed1078822680bf76eefaf43564b5d Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 22 Jul 2026 10:54:28 -0700 Subject: [PATCH 10/26] some stale stuff --- .../TerminalApp/TerminalPage.Protocol.cpp | 3 +- .../TerminalProtocol/TerminalProtocol.idl | 48 ------------------- .../TerminalProtocolComServer.h | 3 +- src/host/proxy/ITerminalProtocol.idl | 8 ++-- 4 files changed, 6 insertions(+), 56 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index 9890c6fd207..4b1a47f4dc7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -3,8 +3,7 @@ // // This file contains the protocol bridge methods for TerminalPage. // These methods are called by the TerminalProtocolComServer to query -// and mutate terminal state. They return typed WinRT structs across -// the DLL boundary. +// 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(). diff --git a/src/cascadia/TerminalProtocol/TerminalProtocol.idl b/src/cascadia/TerminalProtocol/TerminalProtocol.idl index 95f9a09ef1c..21eb29254b7 100644 --- a/src/cascadia/TerminalProtocol/TerminalProtocol.idl +++ b/src/cascadia/TerminalProtocol/TerminalProtocol.idl @@ -77,53 +77,5 @@ namespace Microsoft.Terminal.Protocol UInt64 WindowId; UInt32 Pid; }; - - struct AuthResult - { - Boolean Authenticated; - String ProtocolVersion; - }; - - // Callback interface — implemented by clients (e.g. wtcli) to receive - // push-based event notifications from Terminal. - interface IProtocolEventCallback - { - void OnEvent(String eventJson); - } - - // Server interface — implemented by WindowsTerminal.exe, consumed by wtcli.exe. - // Activated via CoCreateInstance (CLSCTX_LOCAL_SERVER) with a custom class factory. - // Marshaled cross-process via MBM (no proxy/stub DLL needed). - interface IProtocolServer - { - // Meta - AuthResult Authenticate(String token); - String GetCapabilities(); - - // Queries - PaneInfo GetActivePane(); - WindowInfo[] ListWindows(); - TabInfo[] ListTabs(UInt64 windowIdFilter); - PaneInfo[] ListPanes(UInt64 windowIdFilter, UInt32 tabIdFilter); - PaneOutput ReadPaneOutput(Guid sessionId, String source, Int32 maxLines); - ProcessStatus GetProcessStatus(Guid sessionId); - SessionVariable GetSessionVariable(Guid sessionId, String name); - String GetSettings(); - - // Mutations - TabCreationResult CreateTab(UInt64 windowId, String profile, String commandline, String title, String startingDirectory, Boolean suppressAppTitle, Boolean background); - TabCreationResult SplitPane(Guid sessionId, String direction, Single size, String profile, String commandline, Boolean background); - void ClosePane(Guid sessionId); - void SendInput(Guid sessionId, String text); - void FocusPane(Guid sessionId); - void SetSessionVariable(Guid sessionId, String name, String value); - - // Events — push-based via callback - void Subscribe(IProtocolEventCallback callback); - void Unsubscribe(); - - // Client-originated event publishing (agent → WT → listeners) - void SendEvent(String eventJson); - } } diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h index 186f9979c5f..1610755f003 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -25,8 +25,7 @@ class WindowEmperor; // 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); the per-method logic -// and the UI-thread-marshaled page queries are unchanged from the WinRT server. +// failures. Complex results cross the wire as JSON (BSTR) struct __declspec(uuid(__CLSID_TerminalProtocolServer)) TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< Microsoft::WRL::RuntimeClassFlags, diff --git a/src/host/proxy/ITerminalProtocol.idl b/src/host/proxy/ITerminalProtocol.idl index 6070d5fca28..46231ed8cd3 100644 --- a/src/host/proxy/ITerminalProtocol.idl +++ b/src/host/proxy/ITerminalProtocol.idl @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -// Full classic-COM replacement for the WinRT/MBM Microsoft.Terminal.Protocol -// IProtocolServer. Marshaled by the MIDL proxy/stub in OpenConsoleProxy.dll, so -// activation/marshaling does NOT go through the WinRT activation catalog -// (CWinRTActivationStoreCatalog) that crashes on affected combase builds. +// Classic-COM protocol interface consumed by wtcli.exe. Marshaled by the +// MIDL proxy/stub in OpenConsoleProxy.dll, so activation/marshaling does +// not go through the WinRT activation catalog (CWinRTActivationStoreCatalog) +// that crashes on affected combase builds. // // Design (per-method + JSON payloads): scalar inputs stay typed (GUID / BSTR / // integers / float / boolean); every complex result crosses the wire as a JSON From bd38b0892fc5b2dde6b63c6c5a04cfa9718511c0 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 22 Jul 2026 14:53:01 -0700 Subject: [PATCH 11/26] update doc --- doc/wtcli-commands.md | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md index 1dfc6314920..cd2dbe25dc4 100644 --- a/doc/wtcli-commands.md +++ b/doc/wtcli-commands.md @@ -2,13 +2,12 @@ `wtcli` is the CLI client for the Windows Terminal Protocol. It looks up the running Terminal via the `WT_COM_CLSID` environment variable, calls -`CoCreateInstance(CLSCTX_LOCAL_SERVER)` to obtain `IProtocolServer`, and +`CoCreateInstance(CLSCTX_LOCAL_SERVER)` to obtain `ITerminalProtocol`, and exposes a tmux-style command surface over its IDL methods. - Source: `src/tools/wtcli/main.cpp` -- IDL: `src/cascadia/TerminalProtocol/TerminalProtocol.idl` -- Primary in-tree caller: `tools/wta/src/shell/wt_channel/cli_channel.rs` (and - `tools/wta/src/app.rs` for `publish`). +- IDL: `src/host/proxy/ITerminalProtocol.idl` +- Primary in-tree caller: `tools/wta/src/shell/wt_channel/cli_channel.rs`. ## Global flags @@ -35,20 +34,9 @@ scripts) are not counted. | `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`) | -| `wait-for` | — | Block (poll `pane-status`) until the pane process exits. `--interval` is poll period in ms; `--timeout` is seconds (`0` = forever). | `wtcli wait-for -t 3 --timeout 60` | ❌ Not called. (`wta` exposes its own `wait-for` subcommand at `tools/wta/src/main.rs:209`, but its handler polls by shelling out to `wtcli pane-status` in a Rust loop — it does **not** invoke `wtcli wait-for`.) | -| `listen` | — | Long-running. Subscribe to `IProtocolServer` and stream every event JSON line to stdout until Ctrl-C. `-t` filters by pane id; `--event` filters by type and supports a trailing `*` wildcard. | `wtcli --json listen --event "agent.*"` | ✅ `cli_channel.rs` (background listener task) | -| `send-event` | `se` | Publish an event using the `agent_event` envelope: sets `type=event`, `method=agent_event`, fills `params.event` from `-e` and `params.pane_id` from `-p` (or the active pane). Extra params come from the trailing JSON object. | `wtcli send-event -p 3 -e agent.task.completed '{"exit_code":0}'` | ❌ Not called from in-tree code. Documented as the public CLI surface for external agents in `doc/specs/llm-agent-event-integration.md`. | -| `publish` | — | Low-level escape hatch: forwards a raw JSON string straight to `IProtocolServer::SendEvent` with no envelope. Used for events that don't fit the `agent_event` shape (e.g. `autofix_state` routed directly to `TerminalPage`). | `wtcli publish '{"method":"autofix_state","params":{"state":"ready"}}'` | ✅ `tools/wta/src/app.rs` (`publish_event_blocking`) | -| `info` | — | Print `WT_COM_CLSID`, connection status, protocol version, and the server's `GetCapabilities()` method list. | `wtcli --json info` | ✅ `cli_channel.rs` maps `get_capabilities` → `wtcli info` | -| `test-pipe` | — | Smoke test: connect, run `list-windows` + `get_capabilities`, print results. Diagnostic only. | `wtcli test-pipe` | ❌ Not called. Manual diagnostic. | -| `set-env` | `setenv` | Print shell-specific export statements for `WT_COM_CLSID` (`-s powershell\|bash\|cmd`). Output is meant to be `eval`'d / `Invoke-Expression`'d by the caller; it does not modify the current process. | `wtcli set-env -s powershell \| Invoke-Expression` | ❌ Not called. Manual recovery for child shells that didn't inherit `WT_COM_CLSID`. | ## Summary -- **Wired into `wta` runtime (13):** `list-windows`, `list-tabs`, +- **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`, - `listen`, `info`, `publish`. -- **Defined but not invoked from in-tree code (4):** `wait-for`, - `send-event`, `test-pipe`, `set-env`. These remain as public surface for - external agents / shell scripts and for manual debugging. + `new-tab`, `split-pane`, `kill-pane`, `focus-pane`. \ No newline at end of file From 71427de8d2a02ef835a5917feffb344feb45ff53 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Thu, 23 Jul 2026 10:46:09 -0700 Subject: [PATCH 12/26] another stale line --- doc/wtcli-commands.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md index cd2dbe25dc4..0f5b79bec90 100644 --- a/doc/wtcli-commands.md +++ b/doc/wtcli-commands.md @@ -1,9 +1,7 @@ # wtcli Command Reference -`wtcli` is the CLI client for the Windows Terminal Protocol. It looks up the -running Terminal via the `WT_COM_CLSID` environment variable, calls -`CoCreateInstance(CLSCTX_LOCAL_SERVER)` to obtain `ITerminalProtocol`, and -exposes a tmux-style command surface over its IDL methods. +`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` From 20c8410590d64f111cd6183d3e57d6b490cdc152 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Thu, 13 Aug 2026 15:39:41 -0700 Subject: [PATCH 13/26] alias --- .../CascadiaPackage/CascadiaPackage.wapproj | 3 +++ .../CascadiaPackage/Package-Can.appxmanifest | 19 +++++++++++++++++++ .../CascadiaPackage/Package-Dev.appxmanifest | 19 +++++++++++++++++++ .../CascadiaPackage/Package-Pre.appxmanifest | 19 +++++++++++++++++++ .../CascadiaPackage/Package.appxmanifest | 19 +++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 61ad620cee9..d4092e69187 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -18,6 +18,8 @@ false wtd wt + wtdcli + wtcli CA5CAD1A-224A-4171-B13A-F16E576FDD12 @@ -108,6 +110,7 @@ $(OCExecutionAliasName).exe + $(OCCliExecutionAliasName).exe diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index 423c1081f1c..e283373a916 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -144,6 +144,25 @@ + + + + + + + + + + + diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index 70ef00e1432..807a62a1447 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -144,6 +144,25 @@ + + + + + + + + + + + diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index 7109b7e8ae5..2c2f70e2c8e 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -233,6 +233,25 @@ + + + + + + + + + + + diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index 2aae6d1e049..363d6c56be6 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -233,6 +233,25 @@ + + + + + + + + + + + From ff57910c4b1f5406e212d0dd4f6a67a041caa18b Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Tue, 18 Aug 2026 11:25:18 -0700 Subject: [PATCH 14/26] unpackaged fix --- build_output.txt | 195 ++++++++++++++++++ .../TerminalProtocolComServer.cpp | 22 +- src/tools/wtcli/main.cpp | 62 +++++- src/tools/wtcli/wtcli.vcxproj | 2 +- 4 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 build_output.txt diff --git a/build_output.txt b/build_output.txt new file mode 100644 index 00000000000..20333b7ff6c --- /dev/null +++ b/build_output.txt @@ -0,0 +1,195 @@ +Build started at 10:43 AM... +1>------ Build started: Project: wtcli, Configuration: Debug x64 ------ +2>------ Build started: Project: WindowsTerminal, Configuration: Debug x64 ------ +1> dev/pabhoj/wtcli +1> main.cpp +2> TerminalProtocolComServer.cpp +2> Microsoft (R) Manifest Tool +2> +2> Copyright (c) Microsoft Corporation. +2> +2> All rights reserved. +2> +2> Microsoft (R) Manifest Tool +2> +2> Copyright (c) Microsoft Corporation. +2> +2> All rights reserved. +2> +2> Microsoft (R) Manifest Tool +2> +2> Copyright (c) Microsoft Corporation. +2> +2> All rights reserved. +2> +2> Microsoft (R) Manifest Tool +2> +2> Copyright (c) Microsoft Corporation. +2> +2> All rights reserved. +2> +2> Microsoft (R) Manifest Tool +2> +2> Copyright (c) Microsoft Corporation. +2> +2> All rights reserved. +2> +2> Microsoft (R) Manifest Tool +2> +2> Copyright (c) Microsoft Corporation. +2> +2> All rights reserved. +2> +2> Microsoft (R) Manifest Tool +2> +2> Copyright (c) Microsoft Corporation. +2> +2> All rights reserved. +2> +2> WindowsTerminal.vcxproj -> C:\Users\pabhojwa\source\repos\terminal\bin\x64\Debug\WindowsTerminal\WindowsTerminal.exe +1>C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3660,13): error C2039: 'get': is not a member of 'std::basic_string,std::allocator>' +1> WI_ASSERT(m_value.get()); +1> ^ (compiling source file 'main.cpp') +1> C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Tools\MSVC\14.51.36231\include\xstring(589,7): +1> see declaration of 'std::basic_string,std::allocator>' +1> class basic_string { // null-terminated transparent array of elements +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3660,13): +1> the template instantiation context (the oldest one first) is +1> WI_ASSERT(m_value.get()); +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\src\tools\wtcli\main.cpp(71,41): +1> see reference to function template instantiation 'string_type wil::GetModuleFileNameW(HMODULE)' being compiled +1> with +1> [ +1> string_type=std::wstring +1> ] +1> std::filesystem::path dir{ wil::GetModuleFileNameW(nullptr) }; +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(617,5): +1> see reference to function template instantiation 'HRESULT wil::GetModuleFileNameW(HMODULE,string_type &) noexcept' being compiled +1> with +1> [ +1> string_type=std::wstring +1> ] +1> THROW_IF_FAILED((wil::GetModuleFileNameW(module, result))); +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(538,17): +1> see reference to function template instantiation 'HRESULT wil::K32GetModuleFileNameExW(HANDLE,HMODULE,string_type &) noexcept' being compiled +1> with +1> [ +1> string_type=std::wstring +1> ] +1> return wil::GetModuleFileNameExW(nullptr, module, path); +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(528,17): +1> see reference to function template instantiation 'HRESULT wil::AdaptFixedSizeToAllocatedResult(string_type &,wistd::function) noexcept' being compiled +1> with +1> [ +1> string_type=std::wstring +1> ] +1> return wil::AdaptFixedSizeToAllocatedResult(path, wistd::move(adapter)); +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(345,40): +1> see reference to class template instantiation 'wil::details::string_maker' being compiled +1> details::string_maker maker; +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3658,18): +1> while compiling class template member function 'wchar_t *wil::details::string_maker::buffer(void)' +1> wchar_t* buffer() +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(367,13): +1> see the first reference to 'wil::details::string_maker::buffer' in 'wil::AdaptFixedSizeToAllocatedResult' +1> RETURN_IF_FAILED_EXPECTED(callback(maker.buffer(), bufferLength, &valueLengthNeededWithNull)); +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(528,17): +1> see the first reference to 'wil::AdaptFixedSizeToAllocatedResult' in 'wil::K32GetModuleFileNameExW' +1> return wil::AdaptFixedSizeToAllocatedResult(path, wistd::move(adapter)); +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(538,17): +1> see the first reference to 'wil::K32GetModuleFileNameExW' in 'wil::GetModuleFileNameW' +1> return wil::GetModuleFileNameExW(nullptr, module, path); +1> ^ +1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(617,5): +1> see the first reference to 'wil::GetModuleFileNameW' in 'wil::GetModuleFileNameW' +1> THROW_IF_FAILED((wil::GetModuleFileNameW(module, result))); +1> ^ +1>C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3661,28): error C2039: 'get': is not a member of 'std::basic_string,std::allocator>' +1> return m_value.get(); +1> ^ (compiling source file 'main.cpp') +1> C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Tools\MSVC\14.51.36231\include\xstring(589,7): +1> see declaration of 'std::basic_string,std::allocator>' +1> class basic_string { // null-terminated transparent array of elements +1> ^ +2> I2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 3>------ Build started: Project: CascadiaPackage, Configuration: Debug x64 ------ +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/WindowMinimizeButton/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/WindowMaximizeButton/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/WindowCloseButton/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/TerminalPage_PaneMovedAnnouncement_ExistingWindow'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/QuitDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/QuitDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/QuitDialog/CloseButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/Quit'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/PlainText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/LegacyGlobalsPropertyHrefUrl'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/LegacyGlobalsPropertyHrefLabel'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/LegacyGlobalsProperty'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/KeyboardServiceDisabledDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/KeyboardServiceDisabledDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/FeedbackUriValue'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/FeedbackMenuItem'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/ExportSuccess'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/ExportFailure'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CouldNotOpenUriDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAllDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAllDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAllDialog/CloseButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAll'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/Cancel'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/AboutDialog_GettingStartedLink/Content'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Rename/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_PaddingSlider/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeNone'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeImage'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeFontIcon'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeEmoji'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconType/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconEmojiBox/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconEmojiBox/PlaceholderText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconBrowse/Content'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconBox/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_HideIconCheckbox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_HideIconCheckbox/Content'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_General/Header'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_BuiltInIcon/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_BellStyleVisual/Content'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_BellSoundBrowse/Content'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/NullableColorPicker_ColorPickerContentDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/NullableColorPicker_ColorPickerContentDialog/SecondaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/NullableColorPicker_ColorPickerContentDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_LaunchMode/HelpText'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_LaunchMode/Header'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_KeybindingsDisclaimer/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_InitialPosY/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_InitialPosX/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_DisableAnimations/Header'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_TerminalColorsHeader/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_SystemColorsHeader/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_RenameDisclaimerInBox'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_EditButton/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_DeleteDisclaimerInBox'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_DeleteButtonDisclaimerInBox/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_DeleteButton2/Text'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/AddProfile_DuplicateButton/Tag'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/AddProfile_AddNewButton/Tag'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/UnexpectedRendererError'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/SearchBox_NoMatches'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/SearchBox_MatchesAvailable'. The application may throw an exception for certain user configurations when retrieving the resources. +3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/NoticeFontNotFound'. The application may throw an exception for certain user configurations when retrieving the resources. +3>C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Microsoft\VisualStudio\v18.0\AppxPackage\Microsoft.AppXPackage.Targets(2839,5): warning APPX1707: No implementation file was provided for the .winmd file 'C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.WindowsPackageManager.ComInterop.1.8.1911\lib\Microsoft.Management.Deployment.winmd'. To generate registration information in the app manifest, specify the 'Implementation' metadata on the .winmd reference item in the project file. +3>C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Microsoft\VisualStudio\v18.0\AppxPackage\Microsoft.AppXPackage.Targets(2839,5): warning APPX1707: No implementation file was provided for the .winmd file 'C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Web.WebView2.1.0.1661.34\lib\Microsoft.Web.WebView2.Core.winmd'. To generate registration information in the app manifest, specify the 'Implementation' metadata on the .winmd reference item in the project file. +3>Package-Dev.appxmanifest : error APPX0703: Manifest references file 'wtdcli.exe' which is not part of the payload. +========== Build: 1 succeeded, 2 failed, 38 up-to-date, 0 skipped ========== +========== Build completed at 10:44 AM and took 38.478 seconds ========== +========== Deploy: 0 succeeded, 0 failed, 0 skipped ========== +========== Deploy completed at 10:44 AM and took 38.478 seconds ========== diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index b28f1116049..96b6a7bfee6 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -7,6 +7,9 @@ #include "WindowEmperor.h" #include "AppHost.h" +#include +#include "../../types/inc/utils.hpp" + #include #include #include "../TerminalProtocol/ProtocolParsing.h" @@ -27,6 +30,23 @@ namespace Protocol = winrt::Microsoft::Terminal::Protocol; // Static state — set once before registration, never mutated. WindowEmperor* TerminalProtocolComServer::s_emperor = nullptr; +// Returns the CLSID to register/activate the protocol server. +// Packaged: the compile-time per-brand CLSID (unique by package identity). +// Unpackaged/portable: a per-install-path v5 UUID so that multiple portable +// installations in different directories don't share the same registration. +static const GUID& _protocolClsid() noexcept +{ + if (IsPackaged()) + return __uuidof(TerminalProtocolComServer); + + static const GUID portableClsid = []() { + std::filesystem::path dir{ wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()) }; + dir.remove_filename(); + return ::Microsoft::Console::Utils::CreateV5Uuid(__uuidof(TerminalProtocolComServer), std::as_bytes(std::span{ dir.native() })); + }(); + return portableClsid; +} + static DWORD g_comRegistration = 0; static std::shared_mutex g_mtx; static std::thread g_comMtaThread; @@ -69,7 +89,7 @@ try if (SUCCEEDED(regHr)) { regHr = CoRegisterClassObject( - __uuidof(TerminalProtocolComServer), + _protocolClsid(), unk.Get(), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 5ebc29b65b2..18fa3789b6f 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -4,6 +4,11 @@ #include #include +#include +#include +#include +#include + #include "Formatting.h" // Classic-COM Terminal protocol. Generated from @@ -17,9 +22,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -37,10 +44,63 @@ static constexpr CLSID CLSID_TerminalProtocol = { 0x264DE65B, 0xF597, 0x4183, { static constexpr CLSID CLSID_TerminalProtocol = { 0xAD9425AA, 0x1722, 0x4E7B, { 0xA4, 0x51, 0xAA, 0x1D, 0x09, 0x10, 0x6E, 0x83 } }; #endif +static bool _isPackaged() noexcept +{ + static const bool packaged = []() noexcept { + UINT32 n = 0; + return GetCurrentPackageId(&n, nullptr) != APPMODEL_ERROR_NO_PACKAGE; + }(); + return packaged; +} + +// Derive a per-install-path CLSID using the same v5 UUID formula as the server, +// so that unpackaged/portable clients find the correct matching server instance. +static GUID _portableClsid(const GUID& brandClsid) +{ + const GUID brand = brandClsid; // copy before static capture + static const GUID clsid = [brand]() { + // Endian-swap the namespace GUID to network byte order (RFC4122) + auto swapEndian = [](const GUID& g) { + GUID r = g; + r.Data1 = _byteswap_ulong(g.Data1); + r.Data2 = _byteswap_ushort(g.Data2); + r.Data3 = _byteswap_ushort(g.Data3); + return r; + }; + const auto ns = swapEndian(brand); + + std::filesystem::path dir{ wil::GetModuleFileNameW(nullptr) }; + dir.remove_filename(); + const auto& pathBytes = dir.native(); + + BCRYPT_HASH_HANDLE hashHandle = nullptr; + BCryptCreateHash(BCRYPT_SHA1_ALG_HANDLE, &hashHandle, nullptr, 0, nullptr, 0, 0); + BCryptHashData(hashHandle, reinterpret_cast(const_cast(&ns)), sizeof(GUID), 0); + BCryptHashData(hashHandle, reinterpret_cast(const_cast(pathBytes.data())), static_cast(pathBytes.size() * sizeof(wchar_t)), 0); + + std::array buf{}; + BCryptFinishHash(hashHandle, buf.data(), static_cast(buf.size()), 0); + BCryptDestroyHash(hashHandle); + + buf[6] = (buf[6] & 0x0F) | 0x50; // version 5 + buf[8] = (buf[8] & 0x3F) | 0x80; // variant 2 + + GUID result{}; + memcpy_s(&result, sizeof(GUID), buf.data(), sizeof(GUID)); + // Swap back to host byte order + result.Data1 = _byteswap_ulong(result.Data1); + result.Data2 = _byteswap_ushort(result.Data2); + result.Data3 = _byteswap_ushort(result.Data3); + return result; + }(); + return clsid; +} + static winrt::com_ptr ConnectToTerminal(std::string* outVersion = nullptr) { + const auto clsid = _isPackaged() ? CLSID_TerminalProtocol : _portableClsid(CLSID_TerminalProtocol); winrt::com_ptr server; - auto hr = CoCreateInstance(CLSID_TerminalProtocol, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(ITerminalProtocol), server.put_void()); + auto hr = CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(ITerminalProtocol), server.put_void()); if (FAILED(hr)) { fprintf(stderr, "[wtcli] Connection failed: 0x%08X\n", static_cast(hr)); diff --git a/src/tools/wtcli/wtcli.vcxproj b/src/tools/wtcli/wtcli.vcxproj index 42614cb82ff..05f4324d476 100644 --- a/src/tools/wtcli/wtcli.vcxproj +++ b/src/tools/wtcli/wtcli.vcxproj @@ -32,7 +32,7 @@ Console - ole32.lib;WindowsApp.lib;%(AdditionalDependencies) + ole32.lib;WindowsApp.lib;bcrypt.lib;%(AdditionalDependencies) From d0d97e61d4aad86af7a5b9cc0ca1e50e6f8716b4 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Tue, 18 Aug 2026 11:35:37 -0700 Subject: [PATCH 15/26] doc fix --- doc/wtcli-commands.md | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md index 0f5b79bec90..73235668acc 100644 --- a/doc/wtcli-commands.md +++ b/doc/wtcli-commands.md @@ -5,7 +5,6 @@ - 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 @@ -22,19 +21,13 @@ 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 +| `list-windows` | `lsw` | List all Terminal windows. | `wtcli --json list-windows` | — | +| `list-tabs` | `lst` | List tabs in a window. `-w` defaults to the first window. | `wtcli --json list-tabs -w 1` | — | +| `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` | — | +| `active-pane` | — | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json 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` | — | +| `pane-status` | — | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | — | +| `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | — | +| `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"` | — | +| `kill-pane` | `killp` | Close a pane. | `wtcli kill-pane -t 4` | — | +| `focus-pane` | `focusp` | Move focus to the given pane. | `wtcli focus-pane -t 3` | — | From 7c40e2c654da9cf1ec43c1c9b01bf15335fc6a9d Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Tue, 18 Aug 2026 14:34:12 -0700 Subject: [PATCH 16/26] spell --- .github/actions/spelling/allow/allow.txt | 8 + .github/actions/spelling/allow/apis.txt | 5 + .github/actions/spelling/allow/names.txt | 2 + build_output.txt | 195 ----------------------- 4 files changed, 15 insertions(+), 195 deletions(-) delete mode 100644 build_output.txt diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 0e9ddc717e5..36be7caefb4 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -2,6 +2,7 @@ aci allcolors breadcrumb breadcrumbs +capturep ccmp ccon clickable @@ -19,6 +20,7 @@ downsides Emacspeak Fitt flac +focusp FTCS gantt gfm @@ -30,12 +32,15 @@ https hyperlinking hyperlinks Kbds +killp libfuzzer liga Llast Lmid locl lol +lsp +lsw Lorigin maxed megathread @@ -43,6 +48,7 @@ minimalistic mkmk mnt mru +neww notwrapped NTMTo overlined @@ -68,8 +74,10 @@ rubyw runtimes servicebus slnt +splitw ssh stakeholders +submittable subpage subpages sustainability diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 3554798540f..edb9f8d1124 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -17,9 +17,11 @@ CLOSEAPP cmdletbinding colspan COMDLG +combase commandlinetoargv COPYFROMRESOURCE cstdint +CWin CXICON CYICON dataobject @@ -79,6 +81,7 @@ lsass LSHIFT LTGRAY MAINWINDOW +MBM medi memicmp MENUCOMMAND @@ -196,6 +199,8 @@ winstamin wmemcmp wpc WSF +wtcli +wtdcli WWH wwinmain XDocument diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index 1b52d13a22a..0aa1c70e4c5 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -5,6 +5,7 @@ bhoj Bhojwani Bluloco carlos +DFX dhowett Diviness dsafa @@ -59,6 +60,7 @@ Thysell WDX Wellons Westerman +yeelam zadjii Zamor zamora diff --git a/build_output.txt b/build_output.txt deleted file mode 100644 index 20333b7ff6c..00000000000 --- a/build_output.txt +++ /dev/null @@ -1,195 +0,0 @@ -Build started at 10:43 AM... -1>------ Build started: Project: wtcli, Configuration: Debug x64 ------ -2>------ Build started: Project: WindowsTerminal, Configuration: Debug x64 ------ -1> dev/pabhoj/wtcli -1> main.cpp -2> TerminalProtocolComServer.cpp -2> Microsoft (R) Manifest Tool -2> -2> Copyright (c) Microsoft Corporation. -2> -2> All rights reserved. -2> -2> Microsoft (R) Manifest Tool -2> -2> Copyright (c) Microsoft Corporation. -2> -2> All rights reserved. -2> -2> Microsoft (R) Manifest Tool -2> -2> Copyright (c) Microsoft Corporation. -2> -2> All rights reserved. -2> -2> Microsoft (R) Manifest Tool -2> -2> Copyright (c) Microsoft Corporation. -2> -2> All rights reserved. -2> -2> Microsoft (R) Manifest Tool -2> -2> Copyright (c) Microsoft Corporation. -2> -2> All rights reserved. -2> -2> Microsoft (R) Manifest Tool -2> -2> Copyright (c) Microsoft Corporation. -2> -2> All rights reserved. -2> -2> Microsoft (R) Manifest Tool -2> -2> Copyright (c) Microsoft Corporation. -2> -2> All rights reserved. -2> -2> WindowsTerminal.vcxproj -> C:\Users\pabhojwa\source\repos\terminal\bin\x64\Debug\WindowsTerminal\WindowsTerminal.exe -1>C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3660,13): error C2039: 'get': is not a member of 'std::basic_string,std::allocator>' -1> WI_ASSERT(m_value.get()); -1> ^ (compiling source file 'main.cpp') -1> C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Tools\MSVC\14.51.36231\include\xstring(589,7): -1> see declaration of 'std::basic_string,std::allocator>' -1> class basic_string { // null-terminated transparent array of elements -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3660,13): -1> the template instantiation context (the oldest one first) is -1> WI_ASSERT(m_value.get()); -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\src\tools\wtcli\main.cpp(71,41): -1> see reference to function template instantiation 'string_type wil::GetModuleFileNameW(HMODULE)' being compiled -1> with -1> [ -1> string_type=std::wstring -1> ] -1> std::filesystem::path dir{ wil::GetModuleFileNameW(nullptr) }; -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(617,5): -1> see reference to function template instantiation 'HRESULT wil::GetModuleFileNameW(HMODULE,string_type &) noexcept' being compiled -1> with -1> [ -1> string_type=std::wstring -1> ] -1> THROW_IF_FAILED((wil::GetModuleFileNameW(module, result))); -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(538,17): -1> see reference to function template instantiation 'HRESULT wil::K32GetModuleFileNameExW(HANDLE,HMODULE,string_type &) noexcept' being compiled -1> with -1> [ -1> string_type=std::wstring -1> ] -1> return wil::GetModuleFileNameExW(nullptr, module, path); -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(528,17): -1> see reference to function template instantiation 'HRESULT wil::AdaptFixedSizeToAllocatedResult(string_type &,wistd::function) noexcept' being compiled -1> with -1> [ -1> string_type=std::wstring -1> ] -1> return wil::AdaptFixedSizeToAllocatedResult(path, wistd::move(adapter)); -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(345,40): -1> see reference to class template instantiation 'wil::details::string_maker' being compiled -1> details::string_maker maker; -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3658,18): -1> while compiling class template member function 'wchar_t *wil::details::string_maker::buffer(void)' -1> wchar_t* buffer() -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(367,13): -1> see the first reference to 'wil::details::string_maker::buffer' in 'wil::AdaptFixedSizeToAllocatedResult' -1> RETURN_IF_FAILED_EXPECTED(callback(maker.buffer(), bufferLength, &valueLengthNeededWithNull)); -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(528,17): -1> see the first reference to 'wil::AdaptFixedSizeToAllocatedResult' in 'wil::K32GetModuleFileNameExW' -1> return wil::AdaptFixedSizeToAllocatedResult(path, wistd::move(adapter)); -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(538,17): -1> see the first reference to 'wil::K32GetModuleFileNameExW' in 'wil::GetModuleFileNameW' -1> return wil::GetModuleFileNameExW(nullptr, module, path); -1> ^ -1> C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\win32_helpers.h(617,5): -1> see the first reference to 'wil::GetModuleFileNameW' in 'wil::GetModuleFileNameW' -1> THROW_IF_FAILED((wil::GetModuleFileNameW(module, result))); -1> ^ -1>C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\include\wil\resource.h(3661,28): error C2039: 'get': is not a member of 'std::basic_string,std::allocator>' -1> return m_value.get(); -1> ^ (compiling source file 'main.cpp') -1> C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Tools\MSVC\14.51.36231\include\xstring(589,7): -1> see declaration of 'std::basic_string,std::allocator>' -1> class basic_string { // null-terminated transparent array of elements -1> ^ -2> I2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 2> 3>------ Build started: Project: CascadiaPackage, Configuration: Debug x64 ------ -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/WindowMinimizeButton/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/WindowMaximizeButton/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/WindowCloseButton/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/TerminalPage_PaneMovedAnnouncement_ExistingWindow'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/QuitDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/QuitDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/QuitDialog/CloseButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/Quit'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/PlainText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/LegacyGlobalsPropertyHrefUrl'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/LegacyGlobalsPropertyHrefLabel'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/LegacyGlobalsProperty'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/KeyboardServiceDisabledDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/KeyboardServiceDisabledDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/FeedbackUriValue'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/FeedbackMenuItem'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/ExportSuccess'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/ExportFailure'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CouldNotOpenUriDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAllDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAllDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAllDialog/CloseButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/CloseAll'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/Cancel'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'TerminalApp/Resources/AboutDialog_GettingStartedLink/Content'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Rename/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_PaddingSlider/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeNone'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeImage'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeFontIcon'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconTypeEmoji'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconType/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconEmojiBox/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconEmojiBox/PlaceholderText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconBrowse/Content'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_IconBox/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_HideIconCheckbox/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_HideIconCheckbox/Content'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_General/Header'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_BuiltInIcon/[using:Windows.UI.Xaml.Automation]AutomationProperties/Name'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_BellStyleVisual/Content'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Profile_BellSoundBrowse/Content'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/NullableColorPicker_ColorPickerContentDialog/Title'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/NullableColorPicker_ColorPickerContentDialog/SecondaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/NullableColorPicker_ColorPickerContentDialog/PrimaryButtonText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_LaunchMode/HelpText'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_LaunchMode/Header'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_KeybindingsDisclaimer/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_InitialPosY/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_InitialPosX/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/Globals_DisableAnimations/Header'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_TerminalColorsHeader/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_SystemColorsHeader/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_RenameDisclaimerInBox'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_EditButton/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_DeleteDisclaimerInBox'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_DeleteButtonDisclaimerInBox/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/ColorScheme_DeleteButton2/Text'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/AddProfile_DuplicateButton/Tag'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Settings.Editor/Resources/AddProfile_AddNewButton/Tag'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/UnexpectedRendererError'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/SearchBox_NoMatches'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/SearchBox_MatchesAvailable'. The application may throw an exception for certain user configurations when retrieving the resources. -3>GENERATEPROJECTPRIFILE : warning : PRI263: 0xdef01051 - No default or neutral resource given for 'Microsoft.Terminal.Control/Resources/NoticeFontNotFound'. The application may throw an exception for certain user configurations when retrieving the resources. -3>C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Microsoft\VisualStudio\v18.0\AppxPackage\Microsoft.AppXPackage.Targets(2839,5): warning APPX1707: No implementation file was provided for the .winmd file 'C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.WindowsPackageManager.ComInterop.1.8.1911\lib\Microsoft.Management.Deployment.winmd'. To generate registration information in the app manifest, specify the 'Implementation' metadata on the .winmd reference item in the project file. -3>C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Microsoft\VisualStudio\v18.0\AppxPackage\Microsoft.AppXPackage.Targets(2839,5): warning APPX1707: No implementation file was provided for the .winmd file 'C:\Users\pabhojwa\source\repos\terminal\packages\Microsoft.Web.WebView2.1.0.1661.34\lib\Microsoft.Web.WebView2.Core.winmd'. To generate registration information in the app manifest, specify the 'Implementation' metadata on the .winmd reference item in the project file. -3>Package-Dev.appxmanifest : error APPX0703: Manifest references file 'wtdcli.exe' which is not part of the payload. -========== Build: 1 succeeded, 2 failed, 38 up-to-date, 0 skipped ========== -========== Build completed at 10:44 AM and took 38.478 seconds ========== -========== Deploy: 0 succeeded, 0 failed, 0 skipped ========== -========== Deploy completed at 10:44 AM and took 38.478 seconds ========== From f8e1755918b0ed02178fe516ae0d424bac12a8f9 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Tue, 18 Aug 2026 14:39:44 -0700 Subject: [PATCH 17/26] format --- .../TerminalApp/TerminalPage.Protocol.cpp | 4 +- .../TerminalProtocolComServer.cpp | 2 - .../TerminalProtocolComServer.h | 5 +- src/cascadia/WindowsTerminal/WindowEmperor.h | 1 - src/tools/wtcli/Formatting.h | 12 +- src/tools/wtcli/main.cpp | 103 ++++++++++++++---- 6 files changed, 89 insertions(+), 38 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index 4b1a47f4dc7..a3d6818f2d6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -331,9 +331,7 @@ namespace winrt::TerminalApp::implementation if (sourceRoute == ProtocolParsing::PaneOutputSource::Screen) { - const auto startIdx = lines.size() > static_cast(viewHeight) - ? lines.size() - viewHeight - : 0; + const auto startIdx = lines.size() > static_cast(viewHeight) ? lines.size() - viewHeight : 0; std::string content; int lineCount = 0; diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index 96b6a7bfee6..08028c9f1c3 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -708,5 +708,3 @@ try return E_FAIL; } CATCH_RETURN() - - diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h index 1610755f003..50a3bf3737f 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -27,9 +27,7 @@ class WindowEmperor; // (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> +TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass, ITerminalProtocol> { // ── ITerminalProtocol ── STDMETHODIMP GetActivePane(BSTR* json) override; @@ -54,6 +52,5 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass< static HRESULT s_StopListening(); private: - static WindowEmperor* s_emperor; }; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 2ea6245dd0b..beb3913e286 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -22,7 +22,6 @@ struct TerminalProtocolComServer; class WindowEmperor { - public: enum UserMessages : UINT { diff --git a/src/tools/wtcli/Formatting.h b/src/tools/wtcli/Formatting.h index 58e648093c7..262ceb04010 100644 --- a/src/tools/wtcli/Formatting.h +++ b/src/tools/wtcli/Formatting.h @@ -10,10 +10,10 @@ void PrintJson(const Json::Value& val); // Human-readable formatters. The classic-COM server returns its results as // JSON; these render that JSON for interactive (non --json) use. -void FormatWindowsHuman(const Json::Value& windows); // array of window objects -void FormatTabsHuman(const Json::Value& tabs); // array of tab objects -void FormatPanesHuman(const Json::Value& panes); // array of pane objects -void FormatActivePaneHuman(const Json::Value& info); // pane object -void FormatPaneStatusHuman(const Json::Value& status); // process-status object -void FormatCreatedTabHuman(const Json::Value& result); // tab-creation-result object +void FormatWindowsHuman(const Json::Value& windows); // array of window objects +void FormatTabsHuman(const Json::Value& tabs); // array of tab objects +void FormatPanesHuman(const Json::Value& panes); // array of pane objects +void FormatActivePaneHuman(const Json::Value& info); // pane object +void FormatPaneStatusHuman(const Json::Value& status); // process-status object +void FormatCreatedTabHuman(const Json::Value& result); // tab-creation-result object void FormatCreatedPaneHuman(const Json::Value& result); // tab-creation-result object diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 18fa3789b6f..30732a26f77 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -254,10 +254,16 @@ int main() auto* listWindowsCmd = app.add_subcommand("list-windows", "List all windows")->alias("lsw"); listWindowsCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; Json::Value windows; auto hr = CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows); - if (FAILED(hr)) { fprintf(stderr, "ListWindows failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "ListWindows failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) { Json::Value arr(Json::objectValue); @@ -276,7 +282,8 @@ int main() listTabsCmd->add_option("-w,--window-id", listTabsWindowId, "Window ID"); listTabsCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; uint64_t wid = 0; if (listTabsWindowId.empty()) { @@ -298,7 +305,12 @@ int main() } Json::Value tabs; auto hr = CallJson([&](BSTR* j) { return server->ListTabs(wid, j); }, tabs); - if (FAILED(hr)) { fprintf(stderr, "ListTabs failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "ListTabs failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) { Json::Value arr(Json::objectValue); @@ -318,7 +330,8 @@ int main() listPanesCmd->add_option("-w,--window-id", listPanesWindowId, "Window ID"); listPanesCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; uint64_t wid = 0; if (!listPanesWindowId.empty() && !TryParseU64(listPanesWindowId, wid)) { @@ -360,7 +373,12 @@ int main() } Json::Value panes; auto hr = CallJson([&](BSTR* j) { return server->ListPanes(wid, tid, j); }, panes); - if (FAILED(hr)) { fprintf(stderr, "ListPanes failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "ListPanes failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) { Json::Value arr(Json::objectValue); @@ -377,10 +395,16 @@ int main() auto* activePaneCmd = app.add_subcommand("active-pane", "Show the currently active pane"); activePaneCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; Json::Value info; auto hr = CallJson([&](BSTR* j) { return server->GetActivePane(j); }, info); - if (FAILED(hr)) { fprintf(stderr, "GetActivePane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "GetActivePane failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) PrintJson(info); else @@ -394,16 +418,21 @@ int main() auto* capturePaneCmd = app.add_subcommand("capture-pane", "Capture pane output")->alias("capturep"); capturePaneCmd->add_option("-t,--target", capturePaneTarget, "Session ID (GUID)"); capturePaneCmd->add_option("-l,--max-lines", captureMaxLines, "Max lines"); - capturePaneCmd->add_flag("--last-prompt", captureLastPrompt, - "Only return the most recent completed shell prompt (command + output, requires OSC 133 shell integration)"); + capturePaneCmd->add_flag("--last-prompt", captureLastPrompt, "Only return the most recent completed shell prompt (command + output, requires OSC 133 shell integration)"); capturePaneCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; auto sessionId = ResolveSessionId(server.get(), capturePaneTarget); wil::unique_bstr src{ Bstr(captureLastPrompt ? "last_prompt" : "scrollback") }; Json::Value output; auto hr = CallJson([&](BSTR* j) { return server->ReadPaneOutput(sessionId, src.get(), captureMaxLines, j); }, output); - if (FAILED(hr)) { fprintf(stderr, "ReadPaneOutput failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "ReadPaneOutput failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) PrintJson(output); else @@ -416,11 +445,17 @@ int main() paneStatusCmd->add_option("-t,--target", paneStatusTarget, "Session ID (GUID)"); paneStatusCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; auto sessionId = ResolveSessionId(server.get(), paneStatusTarget); Json::Value status; auto hr = CallJson([&](BSTR* j) { return server->GetProcessStatus(sessionId, j); }, status); - if (FAILED(hr)) { fprintf(stderr, "GetProcessStatus failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "GetProcessStatus failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) PrintJson(status); else @@ -435,13 +470,19 @@ int main() newTabCmd->add_option("-d,--cwd", newTabCwd, "Starting directory"); newTabCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; wil::unique_bstr profile{ Bstr("") }, command{ Bstr(newTabCommand) }, title{ Bstr(newTabTitle) }, cwd{ Bstr(newTabCwd) }; Json::Value result; auto hr = CallJson([&](BSTR* j) { return server->CreateTab(0, profile.get(), command.get(), title.get(), cwd.get(), false, true, j); }, result); - if (FAILED(hr)) { fprintf(stderr, "CreateTab failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "CreateTab failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) PrintJson(result); else @@ -461,7 +502,8 @@ int main() splitPaneCmd->add_option("-c,--command", splitPaneCommand, "Command to run"); splitPaneCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; auto sessionId = ResolveSessionId(server.get(), splitPaneTarget); std::string dir; if (!splitPaneDirection.empty()) @@ -477,7 +519,12 @@ int main() auto hr = CallJson([&](BSTR* j) { return server->SplitPane(sessionId, dirB.get(), static_cast(splitSize), profile.get(), command.get(), true, j); }, result); - if (FAILED(hr)) { fprintf(stderr, "SplitPane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "SplitPane failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) PrintJson(result); else @@ -490,10 +537,16 @@ int main() killPaneCmd->add_option("-t,--target", killPaneTarget, "Session ID (GUID)"); killPaneCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; auto sessionId = ResolveSessionId(server.get(), killPaneTarget); auto hr = server->ClosePane(sessionId); - if (FAILED(hr)) { fprintf(stderr, "ClosePane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "ClosePane failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) { Json::Value v; @@ -513,10 +566,16 @@ int main() focusPaneCmd->add_option("-t,--target", focusPaneTarget, "Session ID (GUID)"); focusPaneCmd->callback([&]() { auto server = connect(); - if (!server) return; + if (!server) + return; auto sessionId = ResolveSessionId(server.get(), focusPaneTarget); auto hr = server->FocusPane(sessionId); - if (FAILED(hr)) { fprintf(stderr, "FocusPane failed: 0x%08X\n", static_cast(hr)); exitCode = 1; return; } + if (FAILED(hr)) + { + fprintf(stderr, "FocusPane failed: 0x%08X\n", static_cast(hr)); + exitCode = 1; + return; + } if (jsonMode) { Json::Value v; From 9a0e91094a1d372bded6ed85b731e12ee08f1666 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Thu, 27 Aug 2026 13:36:41 -0700 Subject: [PATCH 18/26] update comment --- .../WindowsTerminal/TerminalProtocolComServer.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index 08028c9f1c3..5a8b38066a8 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -62,11 +62,13 @@ 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. + // The class factory must be registered on an MTA thread, not the STA/UI + // thread. Every ITerminalProtocol method is a synchronous COM call that + // internally creates an IAsyncOperation, calls .get() to block until it + // completes, then returns the result. Each coroutine needs to resume on the + // UI thread via co_await resume_foreground. If registration were on the + // STA/UI thread, .get() would deadlock: the UI thread would be blocked + // waiting for the coroutine, while the coroutine waits for the UI thread. g_comMtaStop.create(wil::EventOptions::ManualReset); wil::unique_event ready(wil::EventOptions::ManualReset); From 73167038b45d09243cb00c3e4c82644278d3312c Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Tue, 22 Sep 2026 15:07:20 -0700 Subject: [PATCH 19/26] worked in prev windows build... --- .../TerminalProtocolComServer.cpp | 75 +++++++++ src/tools/wtcli/main.cpp | 147 +++++++++++++----- 2 files changed, 182 insertions(+), 40 deletions(-) diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index 5a8b38066a8..79347df6db6 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -20,6 +20,7 @@ #include #include +#include using namespace Microsoft::WRL; @@ -138,6 +139,66 @@ HRESULT TerminalProtocolComServer::s_StopListening() // Helpers // ============================================================================ +static DWORD _getIntegrityRid(HANDLE token) +{ + const auto label = wil::get_token_information(token); + const auto sid = label->Label.Sid; + THROW_HR_IF(E_ACCESSDENIED, !IsValidSid(sid)); + + const auto subAuthorityCount = *GetSidSubAuthorityCount(sid); + THROW_HR_IF(E_ACCESSDENIED, subAuthorityCount == 0); + return *GetSidSubAuthority(sid, subAuthorityCount - 1); +} + +static HRESULT _authorizeCaller() noexcept +{ + const auto impersonateHr = CoImpersonateClient(); + if (FAILED(impersonateHr)) + { + LOG_HR(impersonateHr); + return E_ACCESSDENIED; + } + + bool needsRevert = true; + const auto revertOnExit = wil::scope_exit([&]() { + if (needsRevert) + { + LOG_IF_FAILED(CoRevertToSelf()); + } + }); + + bool authorized = false; + try + { + wil::unique_handle callerToken; + THROW_IF_WIN32_BOOL_FALSE(OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, callerToken.put())); + + wil::unique_handle serverToken{ GetCurrentProcessToken() }; + const auto callerUser = wil::get_token_information(callerToken.get()); + const auto serverUser = wil::get_token_information(serverToken.get()); + + THROW_HR_IF(E_ACCESSDENIED, !IsValidSid(callerUser->User.Sid)); + THROW_HR_IF(E_ACCESSDENIED, !IsValidSid(serverUser->User.Sid)); + + authorized = EqualSid(callerUser->User.Sid, serverUser->User.Sid) && + _getIntegrityRid(callerToken.get()) >= _getIntegrityRid(serverToken.get()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + + const auto revertHr = CoRevertToSelf(); + if (FAILED(revertHr)) + { + LOG_HR(revertHr); + return E_ACCESSDENIED; + } + needsRevert = false; + + return authorized ? S_OK : E_ACCESSDENIED; +} + static winrt::TerminalApp::TerminalPage _getPage(AppHost* host) { if (!host) @@ -288,6 +349,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); const auto host = s_emperor->GetMostRecentWindow(); @@ -313,6 +375,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); const auto mostRecent = s_emperor->GetMostRecentWindow(); @@ -346,6 +409,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); Json::Value arr(Json::arrayValue); @@ -384,6 +448,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); Json::Value arr(Json::arrayValue); @@ -422,6 +487,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); const auto src = _hstr(source); @@ -450,6 +516,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); for (const auto& host : s_emperor->GetWindows()) @@ -475,6 +542,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); for (const auto& host : s_emperor->GetWindows()) @@ -500,6 +568,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); const std::filesystem::path settingsPath{ std::wstring_view{ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings::SettingsPath() } @@ -525,6 +594,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); // Find target window. @@ -582,6 +652,7 @@ try { RETURN_HR_IF_NULL(E_POINTER, json); *json = nullptr; + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{}); @@ -622,6 +693,7 @@ CATCH_RETURN() STDMETHODIMP TerminalProtocolComServer::ClosePane(GUID sessionId) try { + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{}); @@ -642,6 +714,7 @@ CATCH_RETURN() STDMETHODIMP TerminalProtocolComServer::SendInput(GUID sessionId, BSTR text) try { + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{}); @@ -671,6 +744,7 @@ CATCH_RETURN() STDMETHODIMP TerminalProtocolComServer::FocusPane(GUID sessionId) try { + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{}); @@ -691,6 +765,7 @@ CATCH_RETURN() STDMETHODIMP TerminalProtocolComServer::SetSessionVariable(GUID sessionId, BSTR name, BSTR value) try { + RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); RETURN_HR_IF(E_INVALIDARG, winrt::guid{ sessionId } == winrt::guid{}); diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 30732a26f77..bb4782c53b0 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -167,44 +167,68 @@ static GUID GuidFromString(const std::string& target) return g; } +static void PrintProtocolError(const char* operation, HRESULT hr) +{ + if (hr == E_ACCESSDENIED) + { + fprintf(stderr, + "[wtcli] %s failed: access denied. Run wtcli at an integrity level at least as high as Terminal.\n", + operation); + } + else + { + fprintf(stderr, "[wtcli] %s failed: 0x%08X\n", operation, static_cast(hr)); + } +} + // Resolve a session id: an explicit GUID string, or the active pane's id. -static GUID ResolveSessionId(ITerminalProtocol* server, const std::string& target) +static bool ResolveSessionId(ITerminalProtocol* server, const std::string& target, GUID& sessionId) { if (!target.empty()) - return GuidFromString(target); + { + sessionId = GuidFromString(target); + return sessionId != GUID{}; + } Json::Value info; const auto hr = CallJson([&](BSTR* j) { return server->GetActivePane(j); }, info); if (FAILED(hr)) { - fprintf(stderr, "[wtcli] Could not resolve active pane (GetActivePane failed: 0x%08X)\n", static_cast(hr)); - return GUID{}; + PrintProtocolError("GetActivePane", hr); + return false; } - const auto sessionId = info["session_id"].asString(); - if (sessionId.empty()) + const auto sessionIdString = info["session_id"].asString(); + if (sessionIdString.empty()) { fprintf(stderr, "[wtcli] No active pane.\n"); - return GUID{}; + return false; } - return GuidFromString(sessionId); + sessionId = GuidFromString(sessionIdString); + return sessionId != GUID{}; } -static uint64_t GetFirstWindowId(ITerminalProtocol* server) +static HRESULT GetFirstWindowId(ITerminalProtocol* server, uint64_t& windowId) { Json::Value windows; - CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows); + RETURN_IF_FAILED(CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows)); if (windows.isArray() && !windows.empty()) - return windows[0u]["window_id"].asUInt64(); - return 0; + { + windowId = windows[0u]["window_id"].asUInt64(); + return S_OK; + } + return S_FALSE; } -static uint32_t GetFirstTabId(ITerminalProtocol* server, uint64_t windowId) +static HRESULT GetFirstTabId(ITerminalProtocol* server, uint64_t windowId, uint32_t& tabId) { Json::Value tabs; - CallJson([&](BSTR* j) { return server->ListTabs(windowId, j); }, tabs); + RETURN_IF_FAILED(CallJson([&](BSTR* j) { return server->ListTabs(windowId, j); }, tabs)); if (tabs.isArray() && !tabs.empty()) - return tabs[0u]["tab_id"].asUInt(); - return UINT32_MAX; + { + tabId = tabs[0u]["tab_id"].asUInt(); + return S_OK; + } + return S_FALSE; } // Allocate a BSTR from a UTF-8 std::string. @@ -260,7 +284,7 @@ int main() auto hr = CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows); if (FAILED(hr)) { - fprintf(stderr, "ListWindows failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("ListWindows", hr); exitCode = 1; return; } @@ -287,12 +311,18 @@ int main() uint64_t wid = 0; if (listTabsWindowId.empty()) { - wid = GetFirstWindowId(server.get()); - if (wid == 0) + const auto resolveHr = GetFirstWindowId(server.get(), wid); + if (FAILED(resolveHr)) + { + PrintProtocolError("ListWindows", resolveHr); + exitCode = 1; + return; + } + if (resolveHr == S_FALSE || wid == 0) { // 0 is the server's "no filter" sentinel, not a real window id; // bail rather than silently listing tabs for ALL windows. - fprintf(stderr, "[wtcli] Could not resolve a window (no windows or ListWindows failed)\n"); + fprintf(stderr, "[wtcli] Could not resolve a window (no windows found)\n"); exitCode = 1; return; } @@ -307,7 +337,7 @@ int main() auto hr = CallJson([&](BSTR* j) { return server->ListTabs(wid, j); }, tabs); if (FAILED(hr)) { - fprintf(stderr, "ListTabs failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("ListTabs", hr); exitCode = 1; return; } @@ -355,18 +385,30 @@ int main() { if (wid == 0) { - wid = GetFirstWindowId(server.get()); - if (wid == 0) + const auto resolveWindowHr = GetFirstWindowId(server.get(), wid); + if (FAILED(resolveWindowHr)) + { + PrintProtocolError("ListWindows", resolveWindowHr); + exitCode = 1; + return; + } + if (resolveWindowHr == S_FALSE || wid == 0) { - fprintf(stderr, "[wtcli] Could not resolve a window (no windows or ListWindows failed)\n"); + fprintf(stderr, "[wtcli] Could not resolve a window (no windows found)\n"); exitCode = 1; return; } } - tid = GetFirstTabId(server.get(), wid); - if (tid == UINT32_MAX) + const auto resolveTabHr = GetFirstTabId(server.get(), wid, tid); + if (FAILED(resolveTabHr)) { - fprintf(stderr, "[wtcli] Could not resolve a tab (no tabs or ListTabs failed)\n"); + PrintProtocolError("ListTabs", resolveTabHr); + exitCode = 1; + return; + } + if (resolveTabHr == S_FALSE || tid == UINT32_MAX) + { + fprintf(stderr, "[wtcli] Could not resolve a tab (no tabs found)\n"); exitCode = 1; return; } @@ -375,7 +417,7 @@ int main() auto hr = CallJson([&](BSTR* j) { return server->ListPanes(wid, tid, j); }, panes); if (FAILED(hr)) { - fprintf(stderr, "ListPanes failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("ListPanes", hr); exitCode = 1; return; } @@ -401,7 +443,7 @@ int main() auto hr = CallJson([&](BSTR* j) { return server->GetActivePane(j); }, info); if (FAILED(hr)) { - fprintf(stderr, "GetActivePane failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("GetActivePane", hr); exitCode = 1; return; } @@ -423,13 +465,18 @@ int main() auto server = connect(); if (!server) return; - auto sessionId = ResolveSessionId(server.get(), capturePaneTarget); + GUID sessionId{}; + if (!ResolveSessionId(server.get(), capturePaneTarget, sessionId)) + { + exitCode = 1; + return; + } wil::unique_bstr src{ Bstr(captureLastPrompt ? "last_prompt" : "scrollback") }; Json::Value output; auto hr = CallJson([&](BSTR* j) { return server->ReadPaneOutput(sessionId, src.get(), captureMaxLines, j); }, output); if (FAILED(hr)) { - fprintf(stderr, "ReadPaneOutput failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("ReadPaneOutput", hr); exitCode = 1; return; } @@ -447,12 +494,17 @@ int main() auto server = connect(); if (!server) return; - auto sessionId = ResolveSessionId(server.get(), paneStatusTarget); + GUID sessionId{}; + if (!ResolveSessionId(server.get(), paneStatusTarget, sessionId)) + { + exitCode = 1; + return; + } Json::Value status; auto hr = CallJson([&](BSTR* j) { return server->GetProcessStatus(sessionId, j); }, status); if (FAILED(hr)) { - fprintf(stderr, "GetProcessStatus failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("GetProcessStatus", hr); exitCode = 1; return; } @@ -479,7 +531,7 @@ int main() }, result); if (FAILED(hr)) { - fprintf(stderr, "CreateTab failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("CreateTab", hr); exitCode = 1; return; } @@ -504,7 +556,12 @@ int main() auto server = connect(); if (!server) return; - auto sessionId = ResolveSessionId(server.get(), splitPaneTarget); + GUID sessionId{}; + if (!ResolveSessionId(server.get(), splitPaneTarget, sessionId)) + { + exitCode = 1; + return; + } std::string dir; if (!splitPaneDirection.empty()) dir = splitPaneDirection; @@ -521,7 +578,7 @@ int main() }, result); if (FAILED(hr)) { - fprintf(stderr, "SplitPane failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("SplitPane", hr); exitCode = 1; return; } @@ -539,11 +596,16 @@ int main() auto server = connect(); if (!server) return; - auto sessionId = ResolveSessionId(server.get(), killPaneTarget); + GUID sessionId{}; + if (!ResolveSessionId(server.get(), killPaneTarget, sessionId)) + { + exitCode = 1; + return; + } auto hr = server->ClosePane(sessionId); if (FAILED(hr)) { - fprintf(stderr, "ClosePane failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("ClosePane", hr); exitCode = 1; return; } @@ -568,11 +630,16 @@ int main() auto server = connect(); if (!server) return; - auto sessionId = ResolveSessionId(server.get(), focusPaneTarget); + GUID sessionId{}; + if (!ResolveSessionId(server.get(), focusPaneTarget, sessionId)) + { + exitCode = 1; + return; + } auto hr = server->FocusPane(sessionId); if (FAILED(hr)) { - fprintf(stderr, "FocusPane failed: 0x%08X\n", static_cast(hr)); + PrintProtocolError("FocusPane", hr); exitCode = 1; return; } From 11c8973d33562f4efc59db55f048758911d858ac Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Tue, 22 Sep 2026 15:59:48 -0700 Subject: [PATCH 20/26] remove capturep --- .github/actions/spelling/allow/allow.txt | 1 - doc/wtcli-commands.md | 1 - .../TerminalApp/TerminalPage.Protocol.cpp | 133 ------------------ src/cascadia/TerminalApp/TerminalPage.h | 1 - src/cascadia/TerminalApp/TerminalPage.idl | 1 - src/cascadia/TerminalControl/ControlCore.cpp | 34 ----- src/cascadia/TerminalControl/ControlCore.h | 1 - src/cascadia/TerminalControl/ControlCore.idl | 1 - src/cascadia/TerminalControl/TermControl.cpp | 4 - src/cascadia/TerminalControl/TermControl.h | 1 - src/cascadia/TerminalControl/TermControl.idl | 1 - .../TerminalProtocol/ProtocolParsing.h | 23 --- .../TerminalProtocol/TerminalProtocol.idl | 13 -- .../TerminalProtocol/ft_fuzzer/fuzzmain.cpp | 8 +- .../TerminalProtocolComServer.cpp | 40 ------ .../TerminalProtocolComServer.h | 1 - src/host/proxy/ITerminalProtocol.idl | 1 - src/tools/wtcli/main.cpp | 33 ----- 18 files changed, 1 insertion(+), 297 deletions(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 36be7caefb4..1fa404705e9 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -2,7 +2,6 @@ aci allcolors breadcrumb breadcrumbs -capturep ccmp ccon clickable diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md index 73235668acc..c6c02904203 100644 --- a/doc/wtcli-commands.md +++ b/doc/wtcli-commands.md @@ -25,7 +25,6 @@ scripts) are not counted. | `list-tabs` | `lst` | List tabs in a window. `-w` defaults to the first window. | `wtcli --json list-tabs -w 1` | — | | `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` | — | | `active-pane` | — | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json 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` | — | | `pane-status` | — | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | — | | `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | — | | `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"` | — | diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index a3d6818f2d6..6ae20dfe8b8 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -16,9 +16,6 @@ #include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" #include -#include "../TerminalProtocol/ProtocolParsing.h" - -namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing; using namespace winrt; using namespace winrt::Windows::Foundation; @@ -240,136 +237,6 @@ namespace winrt::TerminalApp::implementation 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(); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 4e24b9bb276..8db5a9f34ca 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -235,7 +235,6 @@ namespace winrt::TerminalApp::implementation 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); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 87c64103d22..68813169105 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -140,7 +140,6 @@ namespace TerminalApp 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); diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 229ae03030d..3ca144bb4d8 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -2387,40 +2387,6 @@ 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 d9d1a1d0ff0..1a03da9dbef 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -242,7 +242,6 @@ 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 440542a2064..d0ecdb7895a 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -171,7 +171,6 @@ 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 a2eee6f8c94..f549dc62584 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3731,10 +3731,6 @@ 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 afa1851d233..4011c76fc22 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -172,7 +172,6 @@ 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 a7ff37e8a47..faf16be6c84 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -140,7 +140,6 @@ 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 index 6d6ee48b215..dc3a51f5cb7 100644 --- a/src/cascadia/TerminalProtocol/ProtocolParsing.h +++ b/src/cascadia/TerminalProtocol/ProtocolParsing.h @@ -67,27 +67,4 @@ namespace Microsoft::Terminal::Protocol::Parsing // 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 index 21eb29254b7..6efdb9f9aa0 100644 --- a/src/cascadia/TerminalProtocol/TerminalProtocol.idl +++ b/src/cascadia/TerminalProtocol/TerminalProtocol.idl @@ -40,19 +40,6 @@ namespace Microsoft.Terminal.Protocol 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; diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp index e84b561e90c..87c4f7116b8 100644 --- a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp +++ b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT license. // // Fuzzing harness for Terminal Protocol server parsing logic. -// Targets: ParseSplitDirection, ClassifyPaneOutputSource. +// Targets: ParseSplitDirection. // // Built under the Fuzzing MSBuild configuration with LibFuzzer // instrumentation; submittable to OneFuzz via the CI pipeline. @@ -29,12 +29,6 @@ static int FuzzOneInput(const uint8_t* data, size_t size) ProtocolParsing::ParseSplitDirection(input); } - // ── Target 2: ClassifyPaneOutputSource ── - // Feed fuzzed data as a source parameter. - { - ProtocolParsing::ClassifyPaneOutputSource(input); - } - return 0; } diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index 79347df6db6..d077b6c2a03 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -289,17 +289,6 @@ static Json::Value _toJson(const Protocol::PaneInfo& p) 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; @@ -482,35 +471,6 @@ try } CATCH_RETURN() -STDMETHODIMP TerminalProtocolComServer::ReadPaneOutput(GUID sessionId, BSTR source, long maxLines, BSTR* json) -try -{ - RETURN_HR_IF_NULL(E_POINTER, json); - *json = nullptr; - RETURN_IF_FAILED(_authorizeCaller()); - 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 { diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h index 50a3bf3737f..8cb715e214a 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -34,7 +34,6 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClassalias("capturep"); - capturePaneCmd->add_option("-t,--target", capturePaneTarget, "Session ID (GUID)"); - capturePaneCmd->add_option("-l,--max-lines", captureMaxLines, "Max lines"); - capturePaneCmd->add_flag("--last-prompt", captureLastPrompt, "Only return the most recent completed shell prompt (command + output, requires OSC 133 shell integration)"); - capturePaneCmd->callback([&]() { - auto server = connect(); - if (!server) - return; - GUID sessionId{}; - if (!ResolveSessionId(server.get(), capturePaneTarget, sessionId)) - { - exitCode = 1; - return; - } - wil::unique_bstr src{ Bstr(captureLastPrompt ? "last_prompt" : "scrollback") }; - Json::Value output; - auto hr = CallJson([&](BSTR* j) { return server->ReadPaneOutput(sessionId, src.get(), captureMaxLines, j); }, output); - if (FAILED(hr)) - { - PrintProtocolError("ReadPaneOutput", hr); - exitCode = 1; - return; - } - if (jsonMode) - PrintJson(output); - else - printf("%s\n", output["content"].asString().c_str()); - }); - // ── pane-status ── std::string paneStatusTarget; auto* paneStatusCmd = app.add_subcommand("pane-status", "Show pane process status"); From 470ad988c3a4dc562da4d59bead8059dc3fe80a3 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 14 Oct 2026 10:37:49 -0700 Subject: [PATCH 21/26] capturep removed, window name mechanism implemented --- doc/wtcli-commands.md | 55 +++++- .../TerminalApp/TerminalPage.Protocol.cpp | 50 +++++- src/cascadia/TerminalApp/TerminalPage.h | 2 + src/cascadia/TerminalApp/TerminalPage.idl | 2 + .../TerminalProtocol/ProtocolParsing.h | 6 +- .../TerminalProtocol/TerminalProtocol.idl | 7 + .../TerminalProtocol/ft_fuzzer/fuzzmain.cpp | 4 +- .../TerminalProtocolComServer.cpp | 155 ++++++++++++---- .../TerminalProtocolComServer.h | 12 +- .../WindowsTerminal/WindowEmperor.cpp | 142 +++++++++++---- src/cascadia/WindowsTerminal/WindowEmperor.h | 21 ++- src/cascadia/WindowsTerminal/WindowTarget.h | 87 +++++++++ .../WindowsTerminal/WindowsTerminal.vcxproj | 1 + src/host/proxy/ITerminalProtocol.idl | 17 +- src/tools/wtcli/Formatting.cpp | 15 +- src/tools/wtcli/main.cpp | 166 ++++++++---------- 16 files changed, 548 insertions(+), 194 deletions(-) create mode 100644 src/cascadia/WindowsTerminal/WindowTarget.h diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md index c6c02904203..4d40a0a5d4f 100644 --- a/doc/wtcli-commands.md +++ b/doc/wtcli-commands.md @@ -21,12 +21,49 @@ scripts) are not counted. | Command | Alias | What it does | Example | Used in repo | |---------|-------|--------------|---------|--------------| -| `list-windows` | `lsw` | List all Terminal windows. | `wtcli --json list-windows` | — | -| `list-tabs` | `lst` | List tabs in a window. `-w` defaults to the first window. | `wtcli --json list-tabs -w 1` | — | -| `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` | — | -| `active-pane` | — | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json active-pane` | — | -| `pane-status` | — | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | — | -| `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | — | -| `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"` | — | -| `kill-pane` | `killp` | Close a pane. | `wtcli kill-pane -t 4` | — | -| `focus-pane` | `focusp` | Move focus to the given pane. | `wtcli focus-pane -t 3` | — | +| `list-windows` | `lsw` | List all Terminal windows, with each window's `name` and `title`. | `wtcli --json list-windows` | No | +| `list-tabs` | `lst` | List tabs in a window. `-w` picks the window (default: current); `--all` lists every window's tabs. | `wtcli --json list-tabs -w build` | No | +| `list-panes` | `lsp` | List panes in a tab. `-w` picks the window (default: current), `-t` the tab (default: its first); `--all` lists every pane in every window. | `wtcli --json list-panes -t 2` | No | +| `active-pane` | | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json active-pane` | No | +| `pane-status` | | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | No | +| `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd, `-w` window (default: current). Unlike the read-only commands, `-w` here may create the window. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | No | +| `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"` | No | +| `kill-pane` | `killp` | Close a pane. | `wtcli kill-pane -t 4` | No | +| `focus-pane` | `focusp` | Move focus to the given pane. | `wtcli focus-pane -t 3` | No | + +## Targeting a window with `-w` + +`-w,--window` uses the same grammar as `wt --window,-w` (they both go through the +same parser: `src/cascadia/WindowsTerminal/WindowTarget.h`) + +| `-w` value | Means | +|------------|-------| +| *(omitted)* | The current window. | +| `0`, `last` | The current window. | +| a positive integer | The window with that `window_id`, as reported by `list-windows`. | +| `new`, or a negative integer | A new window. | +| anything else | The window with that **name**, as reported by `list-windows`. | + + +### Creating windows with `new-tab` + +`wtcli new-tab -w new` and `wtcli new-tab -w ` create the window +and make the requested tab its first tab. + +A new window builds that tab asynchronously, so by default the server waits up +to 10s for it and then reports the usual `session_id`/`pid`. Pass `--no-wait` to +return as soon as the window exists. Either way, if the tab isn't ready the +result is still a success, flagged with `"pending": true`: + +```console +$ wtcli --json new-tab -w new --no-wait +{"pending":true,"pid":0,"session_id":"{00000000-0000-0000-0000-000000000000}","tab_id":0,"window_id":4} +``` + +When `pending` is true, only `window_id` is meaningful. Follow up with +`wtcli --json list-panes -w ` once the window has started. Scripts +should branch on `pending` rather than testing `session_id` for emptiness. + +Targeting a name that has a **saved workspace** restores that workspace, and the +requested tab is dropped, because a persisted layout takes precedence over requested +startup actions. This matches `wt -w `. diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index 6ae20dfe8b8..2863daa5357 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -84,6 +84,50 @@ namespace winrt::TerminalApp::implementation }().get(); } + // The window's title is the focused tab's title. Reading it walks the XAML + // tab view, which must happen on the UI thread, because the protocol server + // calls this from an MTA COM thread. + hstring TerminalPage::WindowTitle() + { + return [this]() -> IAsyncOperation { + co_await wil::resume_foreground(Dispatcher()); + co_return Title(); + }().get(); + } + + // Wait until this page has finished its startup actions so the initial + // tabs actually exist + IAsyncOperation TerminalPage::WaitForInitialized(uint32_t timeoutMs) + { + auto strong = get_strong(); + co_await wil::resume_foreground(Dispatcher()); + + if (_startupState == StartupState::Initialized) + { + co_return true; + } + + auto done = std::make_shared(wil::EventOptions::ManualReset); + const auto token = Initialized([done](auto&&, auto&&) { + done->SetEvent(); + }); + auto unsubscribe = wil::scope_exit([this, token]() noexcept { + Initialized(token); + }); + + // Re-check now in case Initialized was raised between the check above and the subscription. + if (_startupState == StartupState::Initialized) + { + co_return true; + } + + // LOAD BEARING: we have to get off the UI thread before waiting. + // Initialized is raised via Dispatcher().RunAsync(), so blocking the UI + // thread here would deadlock, since it could never run the handler. + co_await winrt::resume_background(); + co_return done->wait(timeoutMs); + } + Windows::Foundation::IReference TerminalPage::FocusedTabIndex() const { return [this]() -> IAsyncOperation> { @@ -98,7 +142,7 @@ namespace winrt::TerminalApp::implementation } // ============================================================================ - // Queries — return typed WinRT structs + // Queries that return typed WinRT structs // ============================================================================ IAsyncOperation TerminalPage::GetProtocolActivePane() @@ -317,7 +361,7 @@ namespace winrt::TerminalApp::implementation } // ============================================================================ - // Mutations — return typed structs or bool + // Mutations that return typed structs or bool // ============================================================================ IAsyncOperation TerminalPage::SetProtocolSessionVariable(winrt::guid /*sessionId*/, hstring /*name*/, hstring /*value*/) @@ -439,8 +483,6 @@ namespace winrt::TerminalApp::implementation 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'); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 8db5a9f34ca..6f7ee7aabc5 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -231,6 +231,8 @@ namespace winrt::TerminalApp::implementation // Terminal Protocol Bridge Methods uint32_t TabCount() const; + hstring WindowTitle(); + Windows::Foundation::IAsyncOperation WaitForInitialized(uint32_t timeoutMs); Windows::Foundation::IReference FocusedTabIndex() const; Windows::Foundation::IAsyncOperation GetProtocolActivePane(); Windows::Foundation::IAsyncOperation> GetProtocolTabs(); diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 68813169105..a4e51d75025 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -136,6 +136,8 @@ namespace TerminalApp // Terminal Protocol Bridge Methods UInt32 TabCount { get; }; + String WindowTitle { get; }; + Windows.Foundation.IAsyncOperation WaitForInitialized(UInt32 timeoutMs); Windows.Foundation.IReference FocusedTabIndex { get; }; Windows.Foundation.IAsyncOperation GetProtocolActivePane(); Windows.Foundation.IAsyncOperation > GetProtocolTabs(); diff --git a/src/cascadia/TerminalProtocol/ProtocolParsing.h b/src/cascadia/TerminalProtocol/ProtocolParsing.h index dc3a51f5cb7..8dac7130f90 100644 --- a/src/cascadia/TerminalProtocol/ProtocolParsing.h +++ b/src/cascadia/TerminalProtocol/ProtocolParsing.h @@ -11,7 +11,7 @@ namespace Microsoft::Terminal::Protocol::Parsing { - // ── SplitPane direction mapping ── + // SplitPane direction mapping // Mirror of TerminalSettingsModel::SplitDirection enum values. // Kept in sync with ActionArgs.idl. @@ -26,7 +26,7 @@ namespace Microsoft::Terminal::Protocol::Parsing // Map a direction string to a SplitDirection value. // Accepts: "right", "left", "up", "down", "auto", "automatic", - // and legacy values "horizontal" (→ Down) / "vertical" (→ Right). + // and the legacy values "horizontal" (maps to Down) and "vertical" (maps to Right). // Returns Right for unrecognized strings (matching server default). inline SplitDirection ParseSplitDirection(const std::string& direction) { @@ -64,7 +64,7 @@ namespace Microsoft::Terminal::Protocol::Parsing return SplitDirection::Right; } - // Unrecognized — default to Right + // Default to Right for anything unrecognized. return SplitDirection::Right; } } diff --git a/src/cascadia/TerminalProtocol/TerminalProtocol.idl b/src/cascadia/TerminalProtocol/TerminalProtocol.idl index 6efdb9f9aa0..5af4c85419a 100644 --- a/src/cascadia/TerminalProtocol/TerminalProtocol.idl +++ b/src/cascadia/TerminalProtocol/TerminalProtocol.idl @@ -8,6 +8,9 @@ namespace Microsoft.Terminal.Protocol struct WindowInfo { UInt64 WindowId; + // The window's name, as accepted by `wt -w `. Empty when the + // window is unnamed. + String Name; String Title; Boolean IsFocused; UInt32 TabCount; @@ -63,6 +66,10 @@ namespace Microsoft.Terminal.Protocol Guid SessionId; UInt64 WindowId; UInt32 Pid; + // True when a window was created but its first tab isn't ready yet. In + // that case TabId, SessionId and Pid are not yet meaningful, and the + // caller should query the window by WindowId once it has started. + Boolean Pending; }; } diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp index 87c4f7116b8..531d08bed53 100644 --- a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp +++ b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp @@ -12,7 +12,7 @@ namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing; -// Core fuzzing logic — called by both LibFuzzer and the manual main(). +// Core fuzzing logic, called by both LibFuzzer and the manual main(). static int FuzzOneInput(const uint8_t* data, size_t size) { if (size == 0) @@ -22,7 +22,7 @@ static int FuzzOneInput(const uint8_t* data, size_t size) const std::string input(reinterpret_cast(data), size); - // ── Target 1: ParseSplitDirection ── + // Target 1: ParseSplitDirection // Feed fuzzed data as a direction string. Exercises all 8 recognized // values plus arbitrary unrecognized strings. { diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index d077b6c2a03..3d736674a72 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -13,6 +13,7 @@ #include #include #include "../TerminalProtocol/ProtocolParsing.h" +#include "WindowTarget.h" #include #include @@ -28,7 +29,7 @@ namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing; namespace Protocol = winrt::Microsoft::Terminal::Protocol; -// Static state — set once before registration, never mutated. +// Static state, set once before registration and never mutated. WindowEmperor* TerminalProtocolComServer::s_emperor = nullptr; // Returns the CLSID to register/activate the protocol server. @@ -78,7 +79,7 @@ try g_comMtaThread = std::thread([&ready, ®Hr]() { auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); - // Classic-COM class factory (WRL) — marshaled via the OpenConsoleProxy + // Classic-COM class factory (WRL), marshaled via the OpenConsoleProxy // proxy/stub, not WinRT MBM. const auto factory = Make>(); if (!factory) @@ -212,7 +213,6 @@ static winrt::TerminalApp::TerminalPage _getPage(AppHost* host) return root.try_as(); } -// Parse a JSON string into Json::Value static bool _parseJson(const std::string& str, Json::Value& out) { Json::CharReaderBuilder rb; @@ -221,7 +221,7 @@ static bool _parseJson(const std::string& str, Json::Value& out) return Json::parseFromStream(rb, ss, &out, &errs); } -// ── JSON serialization helpers (the wire format is JSON; see wtcli Formatting) ── +// JSON serialization helpers. The wire format is JSON; see wtcli Formatting. static std::string _guidStr(const winrt::guid& g) { @@ -255,6 +255,7 @@ static Json::Value _toJson(const Protocol::WindowInfo& w) { Json::Value v; v["window_id"] = static_cast(w.WindowId); + v["name"] = winrt::to_string(w.Name); v["title"] = winrt::to_string(w.Title); v["is_focused"] = static_cast(w.IsFocused); v["tab_count"] = static_cast(w.TabCount); @@ -320,6 +321,7 @@ static Json::Value _toJson(const Protocol::TabCreationResult& r) v["session_id"] = _guidStr(r.SessionId); v["window_id"] = static_cast(r.WindowId); v["pid"] = static_cast(r.Pid); + v["pending"] = static_cast(r.Pending); return v; } @@ -329,6 +331,20 @@ static winrt::hstring _hstr(BSTR b) return b ? winrt::hstring{ b } : winrt::hstring{}; } +// Resolve a window target for a read-only query. "new" (and negative IDs) are +// rejected because a query should never have the side effect of creating a window. +static HRESULT _resolveWindowForQuery(WindowEmperor* emperor, BSTR windowTarget, const AppHost*& resolved) noexcept +try +{ + const auto target = ParseWindowTarget(_hstr(windowTarget)); + RETURN_HR_IF(E_INVALIDARG, target.Kind == WindowTargetKind::New); + + resolved = emperor->ResolveWindowTarget(target); + RETURN_HR_IF(E_INVALIDARG, !resolved); + return S_OK; +} +CATCH_RETURN() + // ============================================================================ // Queries // ============================================================================ @@ -350,7 +366,7 @@ try 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. + // TerminalPage doesn't know the window ID, so fill it in here. const auto& props = host->Logic().WindowProperties(); info.WindowId = props.WindowId(); @@ -380,11 +396,12 @@ try Protocol::WindowInfo info{}; info.WindowId = props.WindowId(); - info.Title = props.WindowNameForDisplay(); + info.Name = props.WindowName(); info.IsFocused = (host.get() == mostRecent); const auto page = _getPage(host.get()); info.TabCount = page ? page.TabCount() : 0; + info.Title = page ? page.WindowTitle() : winrt::hstring{}; arr.append(_toJson(info)); } @@ -393,7 +410,7 @@ try } CATCH_RETURN() -STDMETHODIMP TerminalProtocolComServer::ListTabs(unsigned __int64 windowIdFilter, BSTR* json) +STDMETHODIMP TerminalProtocolComServer::ListTabs(BSTR windowTarget, boolean allWindows, BSTR* json) try { RETURN_HR_IF_NULL(E_POINTER, json); @@ -401,23 +418,28 @@ try RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); + const AppHost* filter = nullptr; + if (!allWindows) + { + RETURN_IF_FAILED(_resolveWindowForQuery(s_emperor, windowTarget, filter)); + } + Json::Value arr(Json::arrayValue); for (const auto& host : s_emperor->GetWindows()) { - const auto logic = host->Logic(); - if (!logic) + if (filter && host.get() != filter) continue; - const auto& props = logic.WindowProperties(); - if (windowIdFilter != 0 && props.WindowId() != windowIdFilter) + const auto logic = host->Logic(); + if (!logic) continue; const auto page = _getPage(host.get()); if (!page) continue; - const auto windowId = props.WindowId(); + const auto windowId = logic.WindowProperties().WindowId(); const auto tabs = page.GetProtocolTabs().get(); for (uint32_t i = 0; i < tabs.Size(); ++i) { @@ -432,7 +454,7 @@ try } CATCH_RETURN() -STDMETHODIMP TerminalProtocolComServer::ListPanes(unsigned __int64 windowIdFilter, unsigned long tabIdFilter, BSTR* json) +STDMETHODIMP TerminalProtocolComServer::ListPanes(BSTR windowTarget, boolean allWindows, unsigned long tabIdFilter, BSTR* json) try { RETURN_HR_IF_NULL(E_POINTER, json); @@ -440,23 +462,28 @@ try RETURN_IF_FAILED(_authorizeCaller()); RETURN_HR_IF(E_NOT_VALID_STATE, !s_emperor); + const AppHost* filter = nullptr; + if (!allWindows) + { + RETURN_IF_FAILED(_resolveWindowForQuery(s_emperor, windowTarget, filter)); + } + Json::Value arr(Json::arrayValue); for (const auto& host : s_emperor->GetWindows()) { - const auto logic = host->Logic(); - if (!logic) + if (filter && host.get() != filter) continue; - const auto& props = logic.WindowProperties(); - if (windowIdFilter != 0 && props.WindowId() != windowIdFilter) + const auto logic = host->Logic(); + if (!logic) continue; const auto page = _getPage(host.get()); if (!page) continue; - const auto windowId = props.WindowId(); + const auto windowId = logic.WindowProperties().WindowId(); const auto panes = page.GetProtocolPanes(tabIdFilter).get(); for (uint32_t i = 0; i < panes.Size(); ++i) { @@ -542,13 +569,75 @@ CATCH_RETURN() // Mutations // ============================================================================ -STDMETHODIMP TerminalProtocolComServer::CreateTab(unsigned __int64 windowId, +// Create a window for `target` and report its first tab. +// +// The window is created on the main thread via SendMessage, but its first tab +// is only built later during XAML's first layout pass. So we wait up to +// waitTimeoutMs for the page to report itself initialized and then read the +// tab back. If the caller didn't want to wait or the wait expired, we still +// succeeded in creating the window, so report it as Pending rather than failing, +// and let the caller follow up by WindowId. +static HRESULT _createTabInNewWindow(WindowEmperor* emperor, + const WindowTarget& target, + const winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs& args, + unsigned long waitTimeoutMs, + BSTR* json) noexcept +try +{ + WindowEmperor::ProtocolWindowRequest request{ &target, args, 0 }; + SendMessageW(emperor->GetMainWindowForProtocol(), + WindowEmperor::WM_CREATE_PROTOCOL_WINDOW, + 0, + reinterpret_cast(&request)); + RETURN_HR_IF(E_FAIL, request.CreatedWindowId == 0); + + Protocol::TabCreationResult result{}; + result.WindowId = request.CreatedWindowId; + result.Pending = true; + + const auto reportPending = [&]() { + *json = _bstrFromJson(_toJson(result)); + return S_OK; + }; + + if (waitTimeoutMs == 0) + { + return reportPending(); + } + + WindowTarget byId{ WindowTargetKind::Id, request.CreatedWindowId }; + const auto page = _getPage(emperor->ResolveWindowTarget(byId)); + if (!page || !page.WaitForInitialized(waitTimeoutMs).get()) + { + return reportPending(); + } + + // Grab the first pane of the first tab + const auto panes = page.GetProtocolPanes(0).get(); + if (!panes || panes.Size() == 0) + { + return reportPending(); + } + + const auto pane = panes.GetAt(0); + result.TabId = pane.TabId; + result.SessionId = pane.SessionId; + result.Pid = pane.Pid; + result.Pending = false; + + *json = _bstrFromJson(_toJson(result)); + return S_OK; +} +CATCH_RETURN() + +STDMETHODIMP TerminalProtocolComServer::CreateTab(BSTR windowTarget, BSTR profile, BSTR commandline, BSTR title, BSTR startingDirectory, boolean suppressAppTitle, boolean background, + unsigned long waitTimeoutMs, BSTR* json) try { @@ -557,22 +646,8 @@ try RETURN_IF_FAILED(_authorizeCaller()); 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); + const auto target = ParseWindowTarget(_hstr(windowTarget)); - // Build NewTerminalArgs. winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTermArgs; const auto profileH = _hstr(profile); const auto commandlineH = _hstr(commandline); @@ -591,6 +666,17 @@ try newTermArgs.SuppressApplicationTitle(true); } + auto* targetHost = s_emperor->ResolveWindowTarget(target); + if (!targetHost) + { + RETURN_HR_IF(E_INVALIDARG, target.Kind == WindowTargetKind::Id); + RETURN_HR_IF(E_FAIL, target.Kind == WindowTargetKind::Unspecified || target.Kind == WindowTargetKind::Existing); + return _createTabInNewWindow(s_emperor, target, newTermArgs, waitTimeoutMs, json); + } + + const auto page = _getPage(targetHost); + RETURN_HR_IF(E_FAIL, !page); + auto cr = page.CreateProtocolTab(newTermArgs, background != 0).get(); RETURN_HR_IF(E_FAIL, cr.SessionId == winrt::guid{}); @@ -621,7 +707,6 @@ try 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); diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h index 8cb715e214a..1253ebc6183 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -8,7 +8,7 @@ #include "ITerminalProtocol.h" -// Per-brand CLSIDs — same pattern as CTerminalHandoff. +// Per-brand CLSIDs, following the same pattern as CTerminalHandoff. #if defined(WT_BRANDING_RELEASE) #define __CLSID_TerminalProtocolServer "832FDEC7-AA6F-4BAB-85FA-A491405638FC" #elif defined(WT_BRANDING_PREVIEW) @@ -29,22 +29,22 @@ class WindowEmperor; struct __declspec(uuid(__CLSID_TerminalProtocolServer)) TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass, ITerminalProtocol> { - // ── 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 ListTabs(BSTR windowTarget, boolean allWindows, BSTR* json) override; + STDMETHODIMP ListPanes(BSTR windowTarget, boolean allWindows, unsigned long tabIdFilter, 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 CreateTab(BSTR windowTarget, BSTR profile, BSTR commandline, BSTR title, BSTR startingDirectory, boolean suppressAppTitle, boolean background, unsigned long waitTimeoutMs, 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 setup that must be called before s_StartListening(). static void s_setEmperor(WindowEmperor* emperor) noexcept; static HRESULT s_StartListening(); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 2232d8067bc..595816be39e 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -16,6 +16,7 @@ #include "AppHost.h" #include "TerminalProtocolComServer.h" +#include "WindowTarget.h" #include "resource.h" #include "VirtualDesktopUtils.h" #include "../../types/inc/User32Utils.hpp" @@ -258,6 +259,43 @@ AppHost* WindowEmperor::GetWindowByName(std::wstring_view name) const noexcept return nullptr; } +// Resolve a parsed `-w` target to an existing window. +// +// Note that Existing resolves to the most recently activated window +// on any virtual desktop, whereas the commandline path restricts it to +// the current desktop (see _dispatchCommandlineCurrentDesktop). +AppHost* WindowEmperor::ResolveWindowTarget(const WindowTarget& target) const noexcept +{ + switch (target.Kind) + { + case WindowTargetKind::Id: + for (const auto& window : _windows) + { + if (window->Logic().WindowProperties().WindowId() == target.Id) + { + return window.get(); + } + } + return nullptr; + case WindowTargetKind::Name: + for (const auto& window : _windows) + { + if (window->Logic().WindowProperties().WindowName() == target.Name) + { + return window.get(); + } + } + return nullptr; + case WindowTargetKind::Unspecified: + case WindowTargetKind::Existing: + return _mostRecentWindow(); + case WindowTargetKind::New: + return nullptr; + } + + return nullptr; +} + void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args) { _assertIsMainThread(); @@ -342,15 +380,26 @@ void WindowEmperor::OpenWindow(const winrt::hstring& name) _createWindowMaybeRestoringWorkspace(0, name, winrt::TerminalApp::CommandlineArgs{}); } -// Shared tail used by both the commandline dispatch path and OpenWindow(): -// build a WindowRequestedArgs for a new window and, if the request carries a -// name, atomically claim any persisted workspace stored under that name so -// it's restored here and no subsequent caller can pick up the same entry. -void WindowEmperor::_createWindowMaybeRestoringWorkspace(uint64_t windowId, const winrt::hstring& windowName, winrt::TerminalApp::CommandlineArgs args) +// Shared tail used by the commandline dispatch path, OpenWindow(), and the +// protocol server: build a WindowRequestedArgs for a new window and, if the +// request carries a name, atomically claim any persisted workspace stored under +// that name so it's restored here and no subsequent caller can pick up the same +// entry. +// +// startupActions, when provided, describes the window's initial content. Note +// AppHost prefers a PersistedLayout over StartupActions, so a window restored +// from a workspace ignores them. That's the same precedence `wt -w ` +// already has. +void WindowEmperor::_createWindowMaybeRestoringWorkspace(uint64_t windowId, const winrt::hstring& windowName, winrt::TerminalApp::CommandlineArgs args, const Collections::IVector& startupActions) { winrt::TerminalApp::WindowRequestedArgs request{ windowId, std::move(args) }; request.WindowName(windowName); + if (startupActions && startupActions.Size() > 0) + { + request.StartupActions(startupActions); + } + if (!windowName.empty()) { if (const auto layout = ApplicationState::SharedInstance().TakeWorkspace(windowName)) @@ -362,6 +411,31 @@ void WindowEmperor::_createWindowMaybeRestoringWorkspace(uint64_t windowId, cons CreateNewWindow(std::move(request)); } +// Create a window on behalf of the Terminal Protocol server. +// Must run on the main thread, because the protocol server gets here +// by using SendMessage WM_CREATE_PROTOCOL_WINDOW from its MTA thread. +// +// Returns the new window's ID, or 0 on failure. Note the window's tabs do not +// exist yet when this returns, the tabs are built from StartupActions during XAML's +// first layout pass. Callers that need the tab must wait for the page to report +// itself initialized. +uint64_t WindowEmperor::_createProtocolWindow(const WindowTarget& target, const NewTerminalArgs& args) +{ + _assertIsMainThread(); + + // Similar to TerminalPage::_OpenNewWindow + ActionAndArgs newTabAction{}; + newTabAction.Action(ShortcutAction::NewTab); + newTabAction.Args(NewTabArgs{ args }); + auto actions = winrt::single_threaded_vector({ std::move(newTabAction) }); + + const auto windowName = target.Kind == WindowTargetKind::Name ? target.Name : winrt::hstring{}; + _createWindowMaybeRestoringWorkspace(0, windowName, winrt::TerminalApp::CommandlineArgs{}, actions); + + // CreateNewWindow appends to _windows, so the window we just made is last. + return _windows.empty() ? 0 : _windows.back()->Logic().WindowProperties().WindowId(); +} + AppHost* WindowEmperor::_mostRecentWindow() const noexcept { int64_t max = INT64_MIN; @@ -814,42 +888,32 @@ void WindowEmperor::_dispatchCommandline(winrt::TerminalApp::CommandlineArgs arg return; } - const auto parsedTarget = args.TargetWindow(); + const auto target = ParseWindowTarget(args.TargetWindow()); WindowingMode windowingBehavior = WindowingMode::UseNew; uint64_t windowId = 0; winrt::hstring windowName; - // Figure out the windowing behavior the caller wants - // and get the sanitized window ID (if any) and window name (if any). - if (parsedTarget.empty()) + switch (target.Kind) { + case WindowTargetKind::Unspecified: windowingBehavior = _app.Logic().Settings().GlobalSettings().WindowingBehavior(); - } - else if (const auto opt = til::parse_signed(parsedTarget, 10)) - { - // Negative window IDs map to WindowingMode::UseNew. - if (*opt > 0) - { - windowId = gsl::narrow_cast(*opt); - } - else if (*opt == 0) - { - windowingBehavior = WindowingMode::UseExisting; - } - } - else if (parsedTarget == L"last") - { + break; + case WindowTargetKind::Id: + windowId = target.Id; + break; + case WindowTargetKind::Existing: windowingBehavior = WindowingMode::UseExisting; - } - // A window name of "new" maps to WindowingMode::UseNew. - else if (parsedTarget != L"new") - { - windowName = parsedTarget; + break; + case WindowTargetKind::New: + windowingBehavior = WindowingMode::UseNew; + break; + case WindowTargetKind::Name: + windowName = target.Name; + break; } AppHost* window = nullptr; - // Map from the windowing behavior, ID, and name to a window. if (windowId) { window = GetWindowById(windowId); @@ -1219,6 +1283,22 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c } return 0; } + case WM_CREATE_PROTOCOL_WINDOW: + { + auto* request = reinterpret_cast(lParam); + if (request && request->Target) + { + // The protocol server is blocked in SendMessage waiting on this, + // so don't let an exception escape into the window process. + request->CreatedWindowId = 0; + try + { + request->CreatedWindowId = _createProtocolWindow(*request->Target, request->Args); + } + CATCH_LOG(); + } + return 0; + } case WM_NOTIFY_FROM_NOTIFICATION_AREA: switch (LOWORD(lParam)) { @@ -1742,4 +1822,4 @@ void WindowEmperor::_initializeProtocolServer() 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 beb3913e286..a6c7820a811 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -19,6 +19,7 @@ Class Name: class AppHost; struct TerminalProtocolComServer; +struct WindowTarget; class WindowEmperor { @@ -30,6 +31,7 @@ class WindowEmperor WM_IDENTIFY_ALL_WINDOWS, WM_NOTIFY_FROM_NOTIFICATION_AREA, WM_GET_WINDOW_LIST, + WM_CREATE_PROTOCOL_WINDOW, }; // Used by WM_GET_WINDOW_LIST. Callers allocate a vector on their @@ -41,6 +43,20 @@ class WindowEmperor std::wstring Name; }; + // Used by WM_CREATE_PROTOCOL_WINDOW. The Terminal Protocol COM server runs + // on an MTA thread, but windows can only be created on the main/UI thread, + // so it allocates one of these on its stack and passes a pointer through + // LPARAM. SendMessage blocks until the main thread has filled in the result. + struct ProtocolWindowRequest + { + // in: the parsed -w target. Only New and (unmatched) Name reach here. + const WindowTarget* Target; + // in: for window's first tab. + winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs Args; + // out: 0 if the window could not be created. + uint64_t CreatedWindowId; + }; + WindowEmperor(); ~WindowEmperor(); @@ -56,6 +72,8 @@ class WindowEmperor // Protocol server access const std::vector>& GetWindows() const noexcept { return _windows; } AppHost* GetMostRecentWindow() const noexcept { return _mostRecentWindow(); } + AppHost* ResolveWindowTarget(const WindowTarget& target) const noexcept; + HWND GetMainWindowForProtocol() const noexcept { return _window.get(); } private: struct SummonWindowSelectionArgs @@ -69,7 +87,8 @@ class WindowEmperor [[nodiscard]] static LRESULT __stdcall _wndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept; AppHost* _mostRecentWindow() const noexcept; - void _createWindowMaybeRestoringWorkspace(uint64_t windowId, const winrt::hstring& windowName, winrt::TerminalApp::CommandlineArgs args); + void _createWindowMaybeRestoringWorkspace(uint64_t windowId, const winrt::hstring& windowName, winrt::TerminalApp::CommandlineArgs args, const winrt::Windows::Foundation::Collections::IVector& startupActions = nullptr); + uint64_t _createProtocolWindow(const WindowTarget& target, const winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs& args); bool _summonWindow(const SummonWindowSelectionArgs& args) const; void _summonAllWindows() const; void _dispatchSpecialKey(const MSG& msg) const; diff --git a/src/cascadia/WindowsTerminal/WindowTarget.h b/src/cascadia/WindowsTerminal/WindowTarget.h new file mode 100644 index 00000000000..04ea36d908b --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowTarget.h @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Grammar for the `wt --window,-w ` argument, shared by the commandline +// dispatch path (WindowEmperor::_dispatchCommandline) and the Terminal Protocol +// server (TerminalProtocolComServer). Keeping both surfaces on this one parser +// is what stops them from drifting apart. +// +// This classifies the target string and nothing else. It deliberately does NOT +// decide what an unspecified target means: the commandline path resolves that +// from the user's `windowingBehavior` global, while the protocol server +// resolves it to the most recent window. + +#pragma once + +#include "pch.h" + +enum class WindowTargetKind +{ + // "". The caller picks the default. + Unspecified, + // A positive integer: the window with that WindowId(). + Id, + // "0", "-0", or "last": an already-existing window. + Existing, + // "new", or any negative integer. Always a brand new window, and never + // influenced by the `windowingBehavior` global. + New, + // Anything else, including "_quake": the window with that WindowName(). + Name, +}; + +struct WindowTarget +{ + WindowTargetKind Kind{ WindowTargetKind::Unspecified }; + // Only meaningful when Kind == Id. + uint64_t Id{ 0 }; + // Only meaningful when Kind == Name. + winrt::hstring Name; +}; + +// Classify a `-w` target string. +// +// The numeric parse is til::parse_signed with an explicit base of 10, +// which is exactly what the commandline path has always used. That precise call +// is load bearing -- reimplementing it would silently reclassify: +// "+5" -> Name, not Id 5 (a leading '+' is rejected) +// "0x10" -> Name, not Id 16 (base is pinned to 10) +// "0123" -> Id 123, not Id 83 (base is pinned to 10) +// "-0" -> Existing, not New (sign stripped, then 0 == 0) +// "1a", " 1" -> Name, not Id 1 (the whole string must parse) +// "10000000000000000000" -> Name (fits uint64, overflows int64) +inline WindowTarget ParseWindowTarget(const winrt::hstring& target) +{ + if (target.empty()) + { + return { WindowTargetKind::Unspecified }; + } + + if (const auto opt = til::parse_signed(target, 10)) + { + if (*opt > 0) + { + return { WindowTargetKind::Id, gsl::narrow_cast(*opt) }; + } + if (*opt == 0) + { + return { WindowTargetKind::Existing }; + } + // Negative IDs mean a new window. In the original if/else chain this + // case matched no arm at all, and relied on WindowingMode::UseNew being + // the initial value of the local it was assigning to. + return { WindowTargetKind::New }; + } + + if (target == L"last") + { + return { WindowTargetKind::Existing }; + } + // "new" likewise matched no arm in the original chain. + if (target == L"new") + { + return { WindowTargetKind::New }; + } + + return { WindowTargetKind::Name, 0, target }; +} diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index 47df7aeea2b..885d6d6ecd8 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -56,6 +56,7 @@ + diff --git a/src/host/proxy/ITerminalProtocol.idl b/src/host/proxy/ITerminalProtocol.idl index da8c45b2edc..1f8851f6ec2 100644 --- a/src/host/proxy/ITerminalProtocol.idl +++ b/src/host/proxy/ITerminalProtocol.idl @@ -17,31 +17,36 @@ import "unknwn.idl"; // LOAD BEARING (same discipline as ITerminalHandoff): OpenConsoleProxy.dll is a // single shared proxy/stub DLL. Never change or reuse the UUIDs below. -// Terminal protocol — implemented by WindowsTerminal.exe, activated by the +// Terminal protocol, implemented by WindowsTerminal.exe and activated by the // client via CoCreateInstance(CLSCTX_LOCAL_SERVER) with the per-brand CLSID. +// +// Methods that take a `windowTarget` use the same grammar as `wt --window,-w` (see WindowTarget.h) +// i.e. a positive ID, "0"/"last" for an existing window, "new" or a negative ID for a new one +// or any other string as a window name. An empty string resolves to the most recent window. [ object, uuid(D999831E-7EBD-4B14-9D2F-8B09417582D4), pointer_default(unique) ] interface ITerminalProtocol : IUnknown { - // ── Queries (complex results as JSON) ── + // Queries (complex results as JSON) HRESULT GetActivePane([out, retval] BSTR* json); HRESULT ListWindows([out, retval] BSTR* json); - HRESULT ListTabs([in] unsigned hyper windowIdFilter, [out, retval] BSTR* json); - HRESULT ListPanes([in] unsigned hyper windowIdFilter, [in] unsigned long tabIdFilter, [out, retval] BSTR* json); + HRESULT ListTabs([in] BSTR windowTarget, [in] boolean allWindows, [out, retval] BSTR* json); + HRESULT ListPanes([in] BSTR windowTarget, [in] boolean allWindows, [in] unsigned long tabIdFilter, [out, retval] BSTR* json); HRESULT GetProcessStatus([in] GUID sessionId, [out, retval] BSTR* json); HRESULT GetSessionVariable([in] GUID sessionId, [in] BSTR name, [out, retval] BSTR* json); HRESULT GetSettings([out, retval] BSTR* json); - // ── Mutations ── - HRESULT CreateTab([in] unsigned hyper windowId, + // Mutations + HRESULT CreateTab([in] BSTR windowTarget, [in] BSTR profile, [in] BSTR commandline, [in] BSTR title, [in] BSTR startingDirectory, [in] boolean suppressAppTitle, [in] boolean background, + [in] unsigned long waitTimeoutMs, [out, retval] BSTR* json); HRESULT SplitPane([in] GUID sessionId, [in] BSTR direction, diff --git a/src/tools/wtcli/Formatting.cpp b/src/tools/wtcli/Formatting.cpp index ae8885021cc..b0594171f93 100644 --- a/src/tools/wtcli/Formatting.cpp +++ b/src/tools/wtcli/Formatting.cpp @@ -12,7 +12,7 @@ void PrintJson(const Json::Value& val) printf("%s\n", Json::writeString(wb, val).c_str()); } -// ── Human-readable formatters (read the server's JSON) ── +// Human-readable formatters that read the server's JSON. void FormatWindowsHuman(const Json::Value& windows) { @@ -21,11 +21,12 @@ void FormatWindowsHuman(const Json::Value& windows) printf("No windows found.\n"); return; } - printf("%-12s %-30s %s\n", "WINDOW_ID", "TITLE", "FOCUSED"); + printf("%-12s %-20s %-30s %s\n", "WINDOW_ID", "NAME", "TITLE", "FOCUSED"); for (const auto& w : windows) { - printf("%-12llu %-30s %s\n", + printf("%-12llu %-20s %-30s %s\n", static_cast(w["window_id"].asUInt64()), + w["name"].asString().c_str(), w["title"].asString().c_str(), w["is_focused"].asBool() ? "*" : ""); } @@ -85,6 +86,14 @@ void FormatPaneStatusHuman(const Json::Value& status) void FormatCreatedTabHuman(const Json::Value& result) { + // A new window builds the first tab asynchronously, so the server may + // only be able to tell us which window it made. + if (result["pending"].asBool()) + { + printf("Created window %llu; its first tab is still starting.\n", + static_cast(result["window_id"].asUInt64())); + return; + } printf("Created tab %u (session %s)\n", result["tab_id"].asUInt(), result["session_id"].asString().c_str()); diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 265b8da13c8..fa7ae2f3af4 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -14,8 +14,7 @@ // Classic-COM Terminal protocol. Generated from // src/host/proxy/ITerminalProtocol.idl; found via the OpenConsoleProxy IntDir // added to this project's include path. Marshaled by the OpenConsoleProxy -// proxy/stub (NOT WinRT MBM), so activation/marshaling never hits the combase -// WinRT activation catalog. +// proxy/stub, so activation/marshaling never hits the combase WinRT activation catalog. #include "ITerminalProtocol.h" #include @@ -31,9 +30,9 @@ #include #include -// ── Helpers ── +// Helpers -// Per-brand CLSID — matches the server's compile-time selection. +// Per-brand CLSID that matches the server's compile-time selection. #if defined(WT_BRANDING_RELEASE) static constexpr CLSID CLSID_TerminalProtocol = { 0x832FDEC7, 0xAA6F, 0x4BAB, { 0x85, 0xFA, 0xA4, 0x91, 0x40, 0x56, 0x38, 0xFC } }; #elif defined(WT_BRANDING_PREVIEW) @@ -131,8 +130,7 @@ static HRESULT CallJson(F&& call, Json::Value& out) } // A success HRESULT with a null BSTR or malformed JSON is a broken // server contract; surface it as an error so callers' FAILED(hr) - // checks fire immediately instead of proceeding with a - // default-constructed `out`. + // checks fire immediately if (!parsed) hr = E_UNEXPECTED; } @@ -207,22 +205,40 @@ static bool ResolveSessionId(ITerminalProtocol* server, const std::string& targe return sessionId != GUID{}; } -static HRESULT GetFirstWindowId(ITerminalProtocol* server, uint64_t& windowId) +// Allocate a BSTR from a UTF-8 std::string. +static BSTR Bstr(const std::string& s) { - Json::Value windows; - RETURN_IF_FAILED(CallJson([&](BSTR* j) { return server->ListWindows(j); }, windows)); - if (windows.isArray() && !windows.empty()) + return SysAllocString(winrt::to_hstring(s).c_str()); +} + +// Help text for every `-w,--window` option. Matches `wt --window,-w` +static constexpr const char* WindowTargetHelp = + "Window target: an ID, a name, 0/last for the current window, or new for a new one (default: current window)"; + +static constexpr unsigned long NewWindowWaitTimeoutMs = 10000; + +static void PrintWindowTargetError(const char* operation, HRESULT hr, const std::string& windowTarget) +{ + if (hr == E_INVALIDARG) { - windowId = windows[0u]["window_id"].asUInt64(); - return S_OK; + if (windowTarget.empty()) + { + fprintf(stderr, "[wtcli] Could not resolve a window (no windows found)\n"); + } + else + { + fprintf(stderr, "[wtcli] No such window: %s\n", windowTarget.c_str()); + } + return; } - return S_FALSE; + PrintProtocolError(operation, hr); } -static HRESULT GetFirstTabId(ITerminalProtocol* server, uint64_t windowId, uint32_t& tabId) +static HRESULT GetFirstTabId(ITerminalProtocol* server, const std::string& windowTarget, uint32_t& tabId) { Json::Value tabs; - RETURN_IF_FAILED(CallJson([&](BSTR* j) { return server->ListTabs(windowId, j); }, tabs)); + wil::unique_bstr target{ Bstr(windowTarget) }; + RETURN_IF_FAILED(CallJson([&](BSTR* j) { return server->ListTabs(target.get(), false, j); }, tabs)); if (tabs.isArray() && !tabs.empty()) { tabId = tabs[0u]["tab_id"].asUInt(); @@ -231,12 +247,6 @@ static HRESULT GetFirstTabId(ITerminalProtocol* server, uint64_t windowId, uint3 return S_FALSE; } -// Allocate a BSTR from a UTF-8 std::string. -static BSTR Bstr(const std::string& s) -{ - return SysAllocString(winrt::to_hstring(s).c_str()); -} - // Parse a base-10 unsigned 64-bit integer without throwing (unlike std::stoull, // which aborts wtcli on non-numeric input). Returns false on empty, non-numeric, // trailing-garbage, or overflowing input. @@ -254,7 +264,7 @@ static bool TryParseU64(const std::string& s, uint64_t& out) return true; } -// ── Main ── +// Main int main() { @@ -274,7 +284,7 @@ int main() return server; }; - // ── list-windows ── + // list-windows auto* listWindowsCmd = app.add_subcommand("list-windows", "List all windows")->alias("lsw"); listWindowsCmd->callback([&]() { auto server = connect(); @@ -300,44 +310,25 @@ int main() } }); - // ── list-tabs ── - std::string listTabsWindowId; + // list-tabs + std::string listTabsWindow; + bool listTabsAll = false; auto* listTabsCmd = app.add_subcommand("list-tabs", "List tabs in a window")->alias("lst"); - listTabsCmd->add_option("-w,--window-id", listTabsWindowId, "Window ID"); + auto* listTabsWindowOpt = listTabsCmd->add_option("-w,--window", listTabsWindow, WindowTargetHelp); + auto* listTabsWindowIdOpt = listTabsCmd->add_option("--window-id", listTabsWindow, "Deprecated alias for --window")->group(""); + listTabsCmd->add_flag("--all", listTabsAll, "List tabs across every window") + ->excludes(listTabsWindowOpt) + ->excludes(listTabsWindowIdOpt); listTabsCmd->callback([&]() { auto server = connect(); if (!server) return; - uint64_t wid = 0; - if (listTabsWindowId.empty()) - { - const auto resolveHr = GetFirstWindowId(server.get(), wid); - if (FAILED(resolveHr)) - { - PrintProtocolError("ListWindows", resolveHr); - exitCode = 1; - return; - } - if (resolveHr == S_FALSE || wid == 0) - { - // 0 is the server's "no filter" sentinel, not a real window id; - // bail rather than silently listing tabs for ALL windows. - fprintf(stderr, "[wtcli] Could not resolve a window (no windows found)\n"); - exitCode = 1; - return; - } - } - else if (!TryParseU64(listTabsWindowId, wid)) - { - fprintf(stderr, "[wtcli] Invalid --window-id: %s\n", listTabsWindowId.c_str()); - exitCode = 1; - return; - } Json::Value tabs; - auto hr = CallJson([&](BSTR* j) { return server->ListTabs(wid, j); }, tabs); + wil::unique_bstr target{ Bstr(listTabsWindow) }; + auto hr = CallJson([&](BSTR* j) { return server->ListTabs(target.get(), listTabsAll, j); }, tabs); if (FAILED(hr)) { - PrintProtocolError("ListTabs", hr); + PrintWindowTargetError("ListTabs", hr, listTabsWindow); exitCode = 1; return; } @@ -353,22 +344,20 @@ int main() } }); - // ── list-panes ── - std::string listPanesTabId, listPanesWindowId; + // list-panes + std::string listPanesTabId, listPanesWindow; + bool listPanesAll = false; auto* listPanesCmd = app.add_subcommand("list-panes", "List panes in a tab")->alias("lsp"); listPanesCmd->add_option("-t,--tab-id", listPanesTabId, "Tab ID"); - listPanesCmd->add_option("-w,--window-id", listPanesWindowId, "Window ID"); + auto* listPanesWindowOpt = listPanesCmd->add_option("-w,--window", listPanesWindow, WindowTargetHelp); + auto* listPanesWindowIdOpt = listPanesCmd->add_option("--window-id", listPanesWindow, "Deprecated alias for --window")->group(""); + listPanesCmd->add_flag("--all", listPanesAll, "List panes across every window") + ->excludes(listPanesWindowOpt) + ->excludes(listPanesWindowIdOpt); listPanesCmd->callback([&]() { auto server = connect(); if (!server) return; - uint64_t wid = 0; - if (!listPanesWindowId.empty() && !TryParseU64(listPanesWindowId, wid)) - { - fprintf(stderr, "[wtcli] Invalid --window-id: %s\n", listPanesWindowId.c_str()); - exitCode = 1; - return; - } uint32_t tid = UINT32_MAX; if (!listPanesTabId.empty()) { @@ -381,28 +370,12 @@ int main() } tid = static_cast(t); } - if (tid == UINT32_MAX) + if (tid == UINT32_MAX && !listPanesAll) { - if (wid == 0) - { - const auto resolveWindowHr = GetFirstWindowId(server.get(), wid); - if (FAILED(resolveWindowHr)) - { - PrintProtocolError("ListWindows", resolveWindowHr); - exitCode = 1; - return; - } - if (resolveWindowHr == S_FALSE || wid == 0) - { - fprintf(stderr, "[wtcli] Could not resolve a window (no windows found)\n"); - exitCode = 1; - return; - } - } - const auto resolveTabHr = GetFirstTabId(server.get(), wid, tid); + const auto resolveTabHr = GetFirstTabId(server.get(), listPanesWindow, tid); if (FAILED(resolveTabHr)) { - PrintProtocolError("ListTabs", resolveTabHr); + PrintWindowTargetError("ListTabs", resolveTabHr, listPanesWindow); exitCode = 1; return; } @@ -414,10 +387,11 @@ int main() } } Json::Value panes; - auto hr = CallJson([&](BSTR* j) { return server->ListPanes(wid, tid, j); }, panes); + wil::unique_bstr target{ Bstr(listPanesWindow) }; + auto hr = CallJson([&](BSTR* j) { return server->ListPanes(target.get(), listPanesAll, tid, j); }, panes); if (FAILED(hr)) { - PrintProtocolError("ListPanes", hr); + PrintWindowTargetError("ListPanes", hr, listPanesWindow); exitCode = 1; return; } @@ -433,7 +407,7 @@ int main() } }); - // ── active-pane ── + // active-pane auto* activePaneCmd = app.add_subcommand("active-pane", "Show the currently active pane"); activePaneCmd->callback([&]() { auto server = connect(); @@ -453,7 +427,7 @@ int main() FormatActivePaneHuman(info); }); - // ── pane-status ── + // pane-status std::string paneStatusTarget; auto* paneStatusCmd = app.add_subcommand("pane-status", "Show pane process status"); paneStatusCmd->add_option("-t,--target", paneStatusTarget, "Session ID (GUID)"); @@ -481,24 +455,28 @@ int main() FormatPaneStatusHuman(status); }); - // ── new-tab ── - std::string newTabCommand, newTabTitle, newTabCwd; + // new-tab + std::string newTabCommand, newTabTitle, newTabCwd, newTabWindow; + bool newTabNoWait = false; auto* newTabCmd = app.add_subcommand("new-tab", "Create a new tab")->alias("neww"); newTabCmd->add_option("-c,--command", newTabCommand, "Command to run"); newTabCmd->add_option("-n,--title", newTabTitle, "Tab title"); newTabCmd->add_option("-d,--cwd", newTabCwd, "Starting directory"); + newTabCmd->add_option("-w,--window", newTabWindow, WindowTargetHelp); + newTabCmd->add_flag("--no-wait", newTabNoWait, "When creating a window, return as soon as it exists instead of waiting for its first tab"); newTabCmd->callback([&]() { auto server = connect(); if (!server) return; wil::unique_bstr profile{ Bstr("") }, command{ Bstr(newTabCommand) }, title{ Bstr(newTabTitle) }, cwd{ Bstr(newTabCwd) }; + wil::unique_bstr windowTarget{ Bstr(newTabWindow) }; Json::Value result; auto hr = CallJson([&](BSTR* j) { - return server->CreateTab(0, profile.get(), command.get(), title.get(), cwd.get(), false, true, j); + return server->CreateTab(windowTarget.get(), profile.get(), command.get(), title.get(), cwd.get(), false, true, newTabNoWait ? 0 : NewWindowWaitTimeoutMs, j); }, result); if (FAILED(hr)) { - PrintProtocolError("CreateTab", hr); + PrintWindowTargetError("CreateTab", hr, newTabWindow); exitCode = 1; return; } @@ -508,7 +486,7 @@ int main() FormatCreatedTabHuman(result); }); - // ── split-pane ── + // split-pane std::string splitPaneTarget, splitPaneCommand, splitPaneDirection; bool splitHorizontal = false, splitVertical = false; double splitSize = 0.5; @@ -555,7 +533,7 @@ int main() FormatCreatedPaneHuman(result); }); - // ── kill-pane ── + // kill-pane std::string killPaneTarget; auto* killPaneCmd = app.add_subcommand("kill-pane", "Close a pane")->alias("killp"); killPaneCmd->add_option("-t,--target", killPaneTarget, "Session ID (GUID)"); @@ -589,7 +567,7 @@ int main() } }); - // ── focus-pane ── + // focus-pane std::string focusPaneTarget; auto* focusPaneCmd = app.add_subcommand("focus-pane", "Switch focus to a pane")->alias("focusp"); focusPaneCmd->add_option("-t,--target", focusPaneTarget, "Session ID (GUID)"); @@ -623,7 +601,7 @@ int main() } }); - // ── Default (no subcommand) ── + // Default (no subcommand) app.callback([&]() { if (app.get_subcommands().empty()) { From 34f73cb0996e6246bebc447f2cd741a33c696f90 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 14 Oct 2026 11:14:18 -0700 Subject: [PATCH 22/26] /? writes help --- doc/wtcli-commands.md | 27 +++++++++++---------------- src/tools/wtcli/main.cpp | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md index 4d40a0a5d4f..1fe17bcef3a 100644 --- a/doc/wtcli-commands.md +++ b/doc/wtcli-commands.md @@ -14,22 +14,17 @@ ## 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, with each window's `name` and `title`. | `wtcli --json list-windows` | No | -| `list-tabs` | `lst` | List tabs in a window. `-w` picks the window (default: current); `--all` lists every window's tabs. | `wtcli --json list-tabs -w build` | No | -| `list-panes` | `lsp` | List panes in a tab. `-w` picks the window (default: current), `-t` the tab (default: its first); `--all` lists every pane in every window. | `wtcli --json list-panes -t 2` | No | -| `active-pane` | | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json active-pane` | No | -| `pane-status` | | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | No | -| `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd, `-w` window (default: current). Unlike the read-only commands, `-w` here may create the window. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | No | -| `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"` | No | -| `kill-pane` | `killp` | Close a pane. | `wtcli kill-pane -t 4` | No | -| `focus-pane` | `focusp` | Move focus to the given pane. | `wtcli focus-pane -t 3` | No | +| Command | Alias | What it does | Example | +|---------|-------|--------------|---------| +| `list-windows` | `lsw` | List all Terminal windows, with each window's `name` and `title`. | `wtcli --json list-windows` | +| `list-tabs` | `lst` | List tabs in a window. `-w` picks the window (default: current); `--all` lists every window's tabs. | `wtcli --json list-tabs -w build` | +| `list-panes` | `lsp` | List panes in a tab. `-w` picks the window (default: current), `-t` the tab (default: its first); `--all` lists every pane in every window. | `wtcli --json list-panes -t 2` | +| `active-pane` | | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json active-pane` | +| `pane-status` | | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | +| `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd, `-w` window (default: current). Unlike the read-only commands, `-w` here may create the window. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | +| `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"` | +| `kill-pane` | `killp` | Close a pane. | `wtcli kill-pane -t 4` | +| `focus-pane` | `focusp` | Move focus to the given pane. | `wtcli focus-pane -t 3` | ## Targeting a window with `-w` diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index fa7ae2f3af4..f8ebbbe7364 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include // Helpers @@ -613,7 +614,21 @@ int main() try { - app.parse(__argc, __argv); + // `/?` should print the help output, write it to CLI11's --help + std::vector argStorage; + argStorage.reserve(static_cast(__argc)); + for (int i = 0; i < __argc; ++i) + { + argStorage.emplace_back(std::string_view{ __argv[i] } == "/?" ? "--help" : __argv[i]); + } + std::vector argv; + argv.reserve(argStorage.size()); + for (auto& arg : argStorage) + { + argv.push_back(arg.data()); + } + + app.parse(static_cast(argv.size()), argv.data()); } catch (const CLI::ParseError& e) { From a3c88a8e8993f4a4db4af482917d81ce76c1de59 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 21 Oct 2026 14:03:41 -0700 Subject: [PATCH 23/26] bunch of cleanup --- .../TerminalApp/TerminalPage.Protocol.cpp | 45 ------- src/cascadia/TerminalApp/TerminalPage.h | 3 - src/cascadia/TerminalApp/TerminalPage.idl | 4 - .../TerminalProtocol/TerminalProtocol.idl | 19 --- .../TerminalProtocol/TerminalProtocol.vcxproj | 1 - .../TerminalProtocol/ft_fuzzer/fuzzmain.cpp | 4 - .../TerminalProtocolComServer.cpp | 126 +----------------- .../TerminalProtocolComServer.h | 9 +- .../WindowsTerminal/WindowEmperor.cpp | 3 - src/cascadia/WindowsTerminal/WindowEmperor.h | 1 - src/cascadia/WindowsTerminal/WindowTarget.h | 31 +---- .../WindowsTerminal/WindowsTerminal.vcxproj | 8 +- src/host/proxy/ITerminalProtocol.idl | 13 +- src/tools/wtcli/main.cpp | 12 +- src/tools/wtcli/wtcli.vcxproj | 5 - 15 files changed, 20 insertions(+), 264 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index 2863daa5357..5217acce22c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -354,22 +354,10 @@ namespace winrt::TerminalApp::implementation 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 that 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(); @@ -480,39 +468,6 @@ namespace winrt::TerminalApp::implementation co_return false; } - IAsyncOperation TerminalPage::SendProtocolInput(winrt::guid sessionId, hstring text) - { - auto strong = get_strong(); - 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(); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 6f7ee7aabc5..5f6b11803c8 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -238,12 +238,9 @@ namespace winrt::TerminalApp::implementation Windows::Foundation::IAsyncOperation> GetProtocolTabs(); Windows::Foundation::IAsyncOperation> GetProtocolPanes(uint32_t tabIdFilter); 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; diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index a4e51d75025..96b816f97c2 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -2,7 +2,6 @@ // Licensed under the MIT license. import "TaskbarState.idl"; import "Remoting.idl"; -// Microsoft.Terminal.Protocol types are available via project reference (.winmd) namespace TerminalApp { @@ -143,12 +142,9 @@ namespace TerminalApp Windows.Foundation.IAsyncOperation > GetProtocolTabs(); Windows.Foundation.IAsyncOperation > GetProtocolPanes(UInt32 tabIdFilter); 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; diff --git a/src/cascadia/TerminalProtocol/TerminalProtocol.idl b/src/cascadia/TerminalProtocol/TerminalProtocol.idl index 5af4c85419a..128a56e26ff 100644 --- a/src/cascadia/TerminalProtocol/TerminalProtocol.idl +++ b/src/cascadia/TerminalProtocol/TerminalProtocol.idl @@ -1,15 +1,11 @@ // 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; - // The window's name, as accepted by `wt -w `. Empty when the - // window is unnamed. String Name; String Title; Boolean IsFocused; @@ -37,9 +33,6 @@ namespace Microsoft.Terminal.Protocol 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; }; @@ -52,24 +45,12 @@ namespace Microsoft.Terminal.Protocol Boolean HasExitCode; }; - struct SessionVariable - { - Guid SessionId; - String Name; - String Value; - Boolean Exists; - }; - struct TabCreationResult { UInt32 TabId; Guid SessionId; UInt64 WindowId; UInt32 Pid; - // True when a window was created but its first tab isn't ready yet. In - // that case TabId, SessionId and Pid are not yet meaningful, and the - // caller should query the window by WindowId once it has started. Boolean Pending; }; } - diff --git a/src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj b/src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj index 8386ed407c0..24a1c7dd484 100644 --- a/src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj +++ b/src/cascadia/TerminalProtocol/TerminalProtocol.vcxproj @@ -9,7 +9,6 @@ StaticLibrary Console true - true true diff --git a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp index 531d08bed53..236b5df1505 100644 --- a/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp +++ b/src/cascadia/TerminalProtocol/ft_fuzzer/fuzzmain.cpp @@ -3,16 +3,12 @@ // // Fuzzing harness for Terminal Protocol server parsing logic. // Targets: ParseSplitDirection. -// -// 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) diff --git a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp index 3d736674a72..385fc0d5a57 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp @@ -11,7 +11,6 @@ #include "../../types/inc/utils.hpp" #include -#include #include "../TerminalProtocol/ProtocolParsing.h" #include "WindowTarget.h" @@ -29,7 +28,6 @@ namespace ProtocolParsing = Microsoft::Terminal::Protocol::Parsing; namespace Protocol = winrt::Microsoft::Terminal::Protocol; -// Static state, set once before registration and never mutated. WindowEmperor* TerminalProtocolComServer::s_emperor = nullptr; // Returns the CLSID to register/activate the protocol server. @@ -79,8 +77,7 @@ try 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. + // Classic-COM class factory marshaled via the OpenConsoleProxy proxy/stub const auto factory = Make>(); if (!factory) { @@ -233,22 +230,14 @@ static std::string _guidStr(const winrt::guid& g) 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)); + BSTR b = ::SysAllocString(winrt::to_hstring(Json::writeString(wb, v)).c_str()); + THROW_IF_NULL_ALLOC(b); + return b; } static Json::Value _toJson(const Protocol::WindowInfo& w) @@ -297,23 +286,11 @@ static Json::Value _toJson(const Protocol::ProcessStatus& s) 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; @@ -524,47 +501,6 @@ try } CATCH_RETURN() -STDMETHODIMP TerminalProtocolComServer::GetSessionVariable(GUID sessionId, BSTR name, BSTR* json) -try -{ - RETURN_HR_IF_NULL(E_POINTER, json); - *json = nullptr; - RETURN_IF_FAILED(_authorizeCaller()); - 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; - RETURN_IF_FAILED(_authorizeCaller()); - - 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 // ============================================================================ @@ -756,36 +692,6 @@ try } CATCH_RETURN() -STDMETHODIMP TerminalProtocolComServer::SendInput(GUID sessionId, BSTR text) -try -{ - RETURN_IF_FAILED(_authorizeCaller()); - 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 { @@ -806,27 +712,3 @@ try return E_FAIL; } CATCH_RETURN() - -STDMETHODIMP TerminalProtocolComServer::SetSessionVariable(GUID sessionId, BSTR name, BSTR value) -try -{ - RETURN_IF_FAILED(_authorizeCaller()); - 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 index 1253ebc6183..e3484168bb3 100644 --- a/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h +++ b/src/cascadia/WindowsTerminal/TerminalProtocolComServer.h @@ -22,10 +22,7 @@ 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) +// proxy/stub. Complex results cross the wire as JSON (BSTR) struct __declspec(uuid(__CLSID_TerminalProtocolServer)) TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass, ITerminalProtocol> { @@ -35,14 +32,10 @@ TerminalProtocolComServer : public Microsoft::WRL::RuntimeClass(lParam); if (request && request->Target) { - // The protocol server is blocked in SendMessage waiting on this, - // so don't let an exception escape into the window process. request->CreatedWindowId = 0; try { diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index a6c7820a811..c543c21006f 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -115,7 +115,6 @@ class WindowEmperor winrt::TerminalApp::App _app{ nullptr }; std::vector> _windows; - // Protocol server for AI CLI integration void _initializeProtocolServer(); std::vector _hotkeys; NOTIFYICONDATA _notificationIcon{}; diff --git a/src/cascadia/WindowsTerminal/WindowTarget.h b/src/cascadia/WindowsTerminal/WindowTarget.h index 04ea36d908b..88b990a0f57 100644 --- a/src/cascadia/WindowsTerminal/WindowTarget.h +++ b/src/cascadia/WindowsTerminal/WindowTarget.h @@ -3,13 +3,7 @@ // // Grammar for the `wt --window,-w ` argument, shared by the commandline // dispatch path (WindowEmperor::_dispatchCommandline) and the Terminal Protocol -// server (TerminalProtocolComServer). Keeping both surfaces on this one parser -// is what stops them from drifting apart. -// -// This classifies the target string and nothing else. It deliberately does NOT -// decide what an unspecified target means: the commandline path resolves that -// from the user's `windowingBehavior` global, while the protocol server -// resolves it to the most recent window. +// server (TerminalProtocolComServer). #pragma once @@ -23,8 +17,7 @@ enum class WindowTargetKind Id, // "0", "-0", or "last": an already-existing window. Existing, - // "new", or any negative integer. Always a brand new window, and never - // influenced by the `windowingBehavior` global. + // "new", or any negative integer. Always a brand new window. New, // Anything else, including "_quake": the window with that WindowName(). Name, @@ -33,23 +26,12 @@ enum class WindowTargetKind struct WindowTarget { WindowTargetKind Kind{ WindowTargetKind::Unspecified }; - // Only meaningful when Kind == Id. + // Only relevant when Kind == Id. uint64_t Id{ 0 }; - // Only meaningful when Kind == Name. + // Only relevant when Kind == Name. winrt::hstring Name; }; -// Classify a `-w` target string. -// -// The numeric parse is til::parse_signed with an explicit base of 10, -// which is exactly what the commandline path has always used. That precise call -// is load bearing -- reimplementing it would silently reclassify: -// "+5" -> Name, not Id 5 (a leading '+' is rejected) -// "0x10" -> Name, not Id 16 (base is pinned to 10) -// "0123" -> Id 123, not Id 83 (base is pinned to 10) -// "-0" -> Existing, not New (sign stripped, then 0 == 0) -// "1a", " 1" -> Name, not Id 1 (the whole string must parse) -// "10000000000000000000" -> Name (fits uint64, overflows int64) inline WindowTarget ParseWindowTarget(const winrt::hstring& target) { if (target.empty()) @@ -67,9 +49,7 @@ inline WindowTarget ParseWindowTarget(const winrt::hstring& target) { return { WindowTargetKind::Existing }; } - // Negative IDs mean a new window. In the original if/else chain this - // case matched no arm at all, and relied on WindowingMode::UseNew being - // the initial value of the local it was assigning to. + // Negative IDs mean a new window return { WindowTargetKind::New }; } @@ -77,7 +57,6 @@ inline WindowTarget ParseWindowTarget(const winrt::hstring& target) { return { WindowTargetKind::Existing }; } - // "new" likewise matched no arm in the original chain. if (target == L"new") { return { WindowTargetKind::New }; diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index 885d6d6ecd8..f79fa6eb092 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -96,9 +96,11 @@ - + $(OpenConsoleCommonOutDir)TerminalCore\Microsoft.Terminal.Core.winmd diff --git a/src/host/proxy/ITerminalProtocol.idl b/src/host/proxy/ITerminalProtocol.idl index 1f8851f6ec2..02d8489aac3 100644 --- a/src/host/proxy/ITerminalProtocol.idl +++ b/src/host/proxy/ITerminalProtocol.idl @@ -2,15 +2,12 @@ // Licensed under the MIT license. // // Classic-COM protocol interface consumed by wtcli.exe. Marshaled by the -// MIDL proxy/stub in OpenConsoleProxy.dll, so activation/marshaling does -// not go through the WinRT activation catalog (CWinRTActivationStoreCatalog) -// that crashes on affected combase builds. +// MIDL proxy/stub in OpenConsoleProxy.dll. // // Design (per-method + JSON payloads): scalar inputs stay typed (GUID / BSTR / // integers / float / boolean); every complex result crosses the wire as a JSON -// string (BSTR). This avoids SAFEARRAY/struct marshaling entirely and reuses the -// JSON the rest of the system already speaks. The server serializes its existing -// WinRT result structs to JSON; wtcli parses the JSON back for display. +// string (BSTR). The server serializes its existing WinRT result structs to JSON; +// wtcli parses the JSON back for display. import "unknwn.idl"; @@ -35,8 +32,6 @@ import "unknwn.idl"; HRESULT ListTabs([in] BSTR windowTarget, [in] boolean allWindows, [out, retval] BSTR* json); HRESULT ListPanes([in] BSTR windowTarget, [in] boolean allWindows, [in] unsigned long tabIdFilter, [out, retval] BSTR* json); HRESULT GetProcessStatus([in] GUID sessionId, [out, retval] BSTR* json); - HRESULT GetSessionVariable([in] GUID sessionId, [in] BSTR name, [out, retval] BSTR* json); - HRESULT GetSettings([out, retval] BSTR* json); // Mutations HRESULT CreateTab([in] BSTR windowTarget, @@ -56,7 +51,5 @@ import "unknwn.idl"; [in] boolean background, [out, retval] BSTR* json); HRESULT ClosePane([in] GUID sessionId); - HRESULT SendInput([in] GUID sessionId, [in] BSTR text); HRESULT FocusPane([in] GUID sessionId); - HRESULT SetSessionVariable([in] GUID sessionId, [in] BSTR name, [in] BSTR value); }; diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index f8ebbbe7364..1e381c7ddc5 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -10,17 +10,10 @@ #include #include "Formatting.h" - -// Classic-COM Terminal protocol. Generated from -// src/host/proxy/ITerminalProtocol.idl; found via the OpenConsoleProxy IntDir -// added to this project's include path. Marshaled by the OpenConsoleProxy -// proxy/stub, so activation/marshaling never hits the combase WinRT activation catalog. #include "ITerminalProtocol.h" #include - #include - #include #include #include @@ -248,9 +241,8 @@ static HRESULT GetFirstTabId(ITerminalProtocol* server, const std::string& windo return S_FALSE; } -// Parse a base-10 unsigned 64-bit integer without throwing (unlike std::stoull, -// which aborts wtcli on non-numeric input). Returns false on empty, non-numeric, -// trailing-garbage, or overflowing input. +// Parse a base-10 unsigned 64-bit integer. Returns false on empty, non-numeric +// or overflowing input. static bool TryParseU64(const std::string& s, uint64_t& out) { if (s.empty()) diff --git a/src/tools/wtcli/wtcli.vcxproj b/src/tools/wtcli/wtcli.vcxproj index 05f4324d476..b33c0b2f26a 100644 --- a/src/tools/wtcli/wtcli.vcxproj +++ b/src/tools/wtcli/wtcli.vcxproj @@ -26,8 +26,6 @@ _CONSOLE;%(PreprocessorDefinitions) NotUsing - $(IntDir)..\OpenConsoleProxy;%(AdditionalIncludeDirectories) @@ -36,9 +34,6 @@ - {71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF} false From 23a51dfe761b91941bd35d56e20a4177289f5e49 Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 21 Oct 2026 17:05:08 -0700 Subject: [PATCH 24/26] small fixes --- .github/actions/spelling/allow/allow.txt | 4 ++-- doc/wtcli-commands.md | 4 ++-- src/cascadia/TerminalApp/TerminalPage.Protocol.cpp | 2 +- src/tools/wtcli/main.cpp | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 5ffdde00346..a10e295df54 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -50,7 +50,7 @@ minimalistic mkmk mnt mru -neww +newt notwrapped NPCs NTMTo @@ -77,7 +77,7 @@ rubyw runtimes servicebus slnt -splitw +splitp ssh stakeholders submittable diff --git a/doc/wtcli-commands.md b/doc/wtcli-commands.md index 1fe17bcef3a..f751bd86235 100644 --- a/doc/wtcli-commands.md +++ b/doc/wtcli-commands.md @@ -21,8 +21,8 @@ | `list-panes` | `lsp` | List panes in a tab. `-w` picks the window (default: current), `-t` the tab (default: its first); `--all` lists every pane in every window. | `wtcli --json list-panes -t 2` | | `active-pane` | | Return metadata for the currently focused pane. Used by other subcommands as the default `-t` target. | `wtcli --json active-pane` | | `pane-status` | | Report pane process state: `pid`, `state` (`running`/`exited`), and `exit_code` when applicable. | `wtcli --json pane-status -t 3` | -| `new-tab` | `neww` | Create a new tab. `-c` command, `-n` title, `-d` cwd, `-w` window (default: current). Unlike the read-only commands, `-w` here may create the window. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | -| `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"` | +| `new-tab` | `newt` | Create a new tab. `-c` command, `-n` title, `-d` cwd, `-w` window (default: current). Unlike the read-only commands, `-w` here may create the window. | `wtcli --json new-tab -c "pwsh" -n "build" -d C:\src` | +| `split-pane` | `splitp` | 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"` | | `kill-pane` | `killp` | Close a pane. | `wtcli kill-pane -t 4` | | `focus-pane` | `focusp` | Move focus to the given pane. | `wtcli focus-pane -t 3` | diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index 5217acce22c..84077335693 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -268,7 +268,7 @@ namespace winrt::TerminalApp::implementation if (const auto termControl = pane->GetTerminalControl()) { - info.Rows = termControl.ViewHeight(); + info.Rows = termControl.ViewportSize().Height; info.Columns = 0; info.Cwd = termControl.WorkingDirectory(); } diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 1e381c7ddc5..1cc9d8f8cb1 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -451,7 +451,7 @@ int main() // new-tab std::string newTabCommand, newTabTitle, newTabCwd, newTabWindow; bool newTabNoWait = false; - auto* newTabCmd = app.add_subcommand("new-tab", "Create a new tab")->alias("neww"); + auto* newTabCmd = app.add_subcommand("new-tab", "Create a new tab")->alias("newt"); newTabCmd->add_option("-c,--command", newTabCommand, "Command to run"); newTabCmd->add_option("-n,--title", newTabTitle, "Tab title"); newTabCmd->add_option("-d,--cwd", newTabCwd, "Starting directory"); @@ -483,7 +483,7 @@ int main() std::string splitPaneTarget, splitPaneCommand, splitPaneDirection; bool splitHorizontal = false, splitVertical = false; double splitSize = 0.5; - auto* splitPaneCmd = app.add_subcommand("split-pane", "Split a pane")->alias("splitw"); + auto* splitPaneCmd = app.add_subcommand("split-pane", "Split a pane")->alias("splitp"); splitPaneCmd->add_option("-t,--target", splitPaneTarget, "Session ID (GUID)"); splitPaneCmd->add_option("-d,--direction", splitPaneDirection, "Split direction: right|left|up|down|auto"); splitPaneCmd->add_flag("-H,--horizontal", splitHorizontal, "Split horizontally (legacy alias for --direction down)"); From 95efb7b11716afa90540cd12443c6f76f3a1b56c Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Wed, 21 Oct 2026 17:07:14 -0700 Subject: [PATCH 25/26] er, format I guess --- src/cascadia/TerminalApp/TerminalPage.Protocol.cpp | 9 ++++++--- src/tools/wtcli/main.cpp | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp index 84077335693..30efff8c418 100644 --- a/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.Protocol.cpp @@ -81,7 +81,8 @@ namespace winrt::TerminalApp::implementation return [this]() -> IAsyncOperation { co_await wil::resume_foreground(Dispatcher()); co_return NumberOfTabs(); - }().get(); + }() + .get(); } // The window's title is the focused tab's title. Reading it walks the XAML @@ -92,7 +93,8 @@ namespace winrt::TerminalApp::implementation return [this]() -> IAsyncOperation { co_await wil::resume_foreground(Dispatcher()); co_return Title(); - }().get(); + }() + .get(); } // Wait until this page has finished its startup actions so the initial @@ -138,7 +140,8 @@ namespace winrt::TerminalApp::implementation co_return Windows::Foundation::IReference(idx.value()); } co_return nullptr; - }().get(); + }() + .get(); } // ============================================================================ diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 1cc9d8f8cb1..2550278794f 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -466,7 +466,8 @@ int main() Json::Value result; auto hr = CallJson([&](BSTR* j) { return server->CreateTab(windowTarget.get(), profile.get(), command.get(), title.get(), cwd.get(), false, true, newTabNoWait ? 0 : NewWindowWaitTimeoutMs, j); - }, result); + }, + result); if (FAILED(hr)) { PrintWindowTargetError("CreateTab", hr, newTabWindow); @@ -513,7 +514,8 @@ int main() Json::Value result; auto hr = CallJson([&](BSTR* j) { return server->SplitPane(sessionId, dirB.get(), static_cast(splitSize), profile.get(), command.get(), true, j); - }, result); + }, + result); if (FAILED(hr)) { PrintProtocolError("SplitPane", hr); From 9ae270c569fe80b196744ebbe3be528c61750e2b Mon Sep 17 00:00:00 2001 From: Pankaj Bhojwani Date: Fri, 23 Oct 2026 10:45:48 -0700 Subject: [PATCH 26/26] remove from stable builds --- .../CascadiaPackage/CascadiaPackage.wapproj | 1 + .../CascadiaPackage/Package.appxmanifest | 21 ------------------- .../WindowsTerminal/WindowEmperor.cpp | 11 ++++++---- src/features.xml | 7 +++++++ src/tools/wtcli/main.cpp | 6 ++++++ 5 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index d4092e69187..3dc29f9d4bc 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -101,6 +101,7 @@ + <_FilteredNonWapProjProjectOutput Remove="@(_FilteredNonWapProjProjectOutput)" Condition="'$(WindowsTerminalBranding)' == 'Release' and '%(Filename)' == 'wtcli'" /> <_TemporaryFilteredWapProjOutput Include="@(_FilteredNonWapProjProjectOutput)" /> <_FilteredNonWapProjProjectOutput Remove="@(_TemporaryFilteredWapProjOutput)" /> diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest index 363d6c56be6..679f3b9a526 100644 --- a/src/cascadia/CascadiaPackage/Package.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -202,7 +202,6 @@ - @@ -212,7 +211,6 @@ - @@ -233,25 +231,6 @@ - - - - - - - - - - - diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 86be147337a..c6bd9d1c3b1 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -1826,8 +1826,11 @@ void WindowEmperor::_checkWindowsForNotificationIcon() 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"); + if constexpr (Feature_WtCli::IsEnabled()) + { + // 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"); + } } diff --git a/src/features.xml b/src/features.xml index ee6c5be6399..2abac55f256 100644 --- a/src/features.xml +++ b/src/features.xml @@ -194,4 +194,11 @@ + + Feature_WtCli + Enables the wtcli client and Terminal Protocol server. + AlwaysEnabled + + + diff --git a/src/tools/wtcli/main.cpp b/src/tools/wtcli/main.cpp index 2550278794f..23ac84eec02 100644 --- a/src/tools/wtcli/main.cpp +++ b/src/tools/wtcli/main.cpp @@ -91,6 +91,12 @@ static GUID _portableClsid(const GUID& brandClsid) static winrt::com_ptr ConnectToTerminal(std::string* outVersion = nullptr) { + if constexpr (!Feature_WtCli::IsEnabled()) + { + fprintf(stderr, "[wtcli] This feature is disabled in this build.\n"); + return nullptr; + } + const auto clsid = _isPackaged() ? CLSID_TerminalProtocol : _portableClsid(CLSID_TerminalProtocol); winrt::com_ptr server; auto hr = CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(ITerminalProtocol), server.put_void());