Skip to content

Implement wtcli, an application to query details about an existing Terminal instance - #20461

Open
PankajBhojwani wants to merge 18 commits into
mainfrom
dev/pabhoj/wtcli
Open

Implement wtcli, an application to query details about an existing Terminal instance#20461
PankajBhojwani wants to merge 18 commits into
mainfrom
dev/pabhoj/wtcli

Conversation

@PankajBhojwani

@PankajBhojwani PankajBhojwani commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary of the Pull Request

Implements wtcli, a new command-line client for Windows Terminal Protocol: a local COM control surface that lets external processes inspect and drive a running Windows Terminal instance. It provides a tmux-style command vocabulary for the terminal.

wtcli currently exposes 10 commands:

  • Inspect: list-windows (lsw), list-tabs (lst), list-panes
    (lsp), active-pane, pane-status
  • Read output: capture-pane (capturep) — including --last-prompt to
    grab the most recent completed shell command via OSC 133 shell integration
  • Mutate: new-tab (neww), split-pane (splitw), kill-pane
    (killp), focus-pane (focusp)

Every command supports --json for machine-readable output, otherwise results are rendered as human-readable tables.

References and Relevant Issues

Detailed Description of the Pull Request / Additional comments

A client activates the server directly with CoCreateInstance using a per-brand CLSID (Release/Preview/Canary/Dev, selected at compile time), and issues typed commands.

Components:
1. wtcli.exe: the CLI client (src/tools/wtcli/)
The user-facing tool. Parses subcommands, activates the protocol server via WT_COM_CLSID, and calls the interface methods.

2. ITerminalProtocol: the classic-COM interface (src/host/proxy/ITerminalProtocol.idl)
The contract between client and Terminal. A classic-COM interface marshaled by the MIDL proxy/stub in OpenConsoleProxy.dll (registered per-brand in the app manifests). Scalar arguments stay typed; every complex result crosses the boundary as a JSON BSTR.

3. TerminalProtocolComServer: the in-proc server (src/cascadia/WindowsTerminal/)
Implements ITerminalProtocol inside WindowsTerminal.exe.

4. TerminalPage protocol bridge (src/cascadia/TerminalApp/TerminalPage.Protocol.cpp)
The UI-thread methods that actually query and mutate live terminal state (windows, tabs, panes, process status, pane output) and return typed results.

5. TerminalProtocol shared library (src/cascadia/TerminalProtocol/)
Hosts the shared WinRT struct types (WindowInfo, TabInfo, PaneInfo, etc) consumed in-process by both the page bridge and the server. Also provides ProtocolParsing.h, a header of parsing helpers (ParseSplitDirection, ClassifyPaneOutputSource) shared by the server and the fuzzer.

6. Protocol fuzzer (src/cascadia/TerminalProtocol/ft_fuzzer/)
A LibFuzzer harness targeting the parsing functions (ParseSplitDirection, ClassifyPaneOutputSource).

Validation Steps Performed

In a dev build of terminal, all 10 of the commands work

PR Checklist

  • Closes #xxx
  • Tests added/passed
  • Documentation updated
    • If checked, please file a pull request on our docs repo and link it here: #xxx
  • Schema updated (if necessary)

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check-spelling found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@github-actions

This comment has been minimized.

@PankajBhojwani
PankajBhojwani marked this pull request as ready for review July 23, 2026 17:46
@github-actions

This comment has been minimized.

Steven Pribilinskiy (steven-pribilinskiy) added a commit to steven-pribilinskiy/terminal that referenced this pull request Aug 16, 2026
…pane

Windows Terminal has no way for another process to say "read that pane" or
"type this into that pane". The only channel available from outside is UI
Automation plus SendKeys, and it is broken in two ways that cannot be fixed
from outside the process: synthesised keystrokes interleave with the user's
own typing (a real capture: /rename my-session arrived as //rreenn/ramemnea
mne), and they can only reach the foreground window, addressing a whole
window rather than a pane. With any split on screen the write lands
somewhere unpredictable.

So serve a named pipe, \\.\pipe\wt-control-<pid>, one per Terminal process.
Four operations, newline-delimited UTF-8 JSON: ping, list-panes,
capture-pane, send-input. Text goes to the connection, not the keyboard.

Guarantees the design is built around:

* No focus involvement. No SetForegroundWindow, no activation, no tab
  switching, no Focus(). It works against a minimised window on another
  virtual desktop with the pane in a background tab, and the user's focus
  ends up exactly where it started.
* send-input takes a requireContains needle that is re-checked against the
  pane's screen atomically with the write, on the same trip to the UI
  thread. If the needle has gone, nothing is written. Between a client's
  find and its write, tabs can be reordered and panes closed; the failure
  that guard prevents is a command typed into somebody else's shell.
* Verbatim writes. Nothing added, escaped or coalesced - a single write
  containing both text and \r is read by some TUIs as a bulk paste and
  never submits, so the caller sends Enter as its own call.
* No UI-thread blocking. Accept, read, parse, serialise and process-name
  lookups happen on the pipe threads; only the pane touch is marshalled,
  via WM_CONTROL_PIPE_REQUEST to WindowEmperor's message window.

Security: the pipe is created with an explicit DACL granting the current
user's SID and nothing else, protected against inheritance, and rejects
remote clients. The op set cannot run a command, touch a file, change a
setting or create a pane. Connections are traced; the text being written
never is. The `controlPipe` global setting (default true here) is a kill
switch that takes effect on settings reload without a restart.

Upstream has refused wt send-input twice, in microsoft#9368 and PR microsoft#20106, and the
automation surface they are building in PR microsoft#20461 (wtcli, over COM) leaves
input out on purpose and cannot be reached from WSL. This is fork-only;
list-panes reports each pane's connection SessionId as well, which is how
upstream addresses panes, so the two can be reconciled later.

Documented in doc/control-pipe.md. Wire format parsing and the pane-id
round trip are covered by UnitTests_Control/ControlPipeProtocolTests.cpp;
the pipe itself was verified end to end against a dev build with both
windows minimised - split panes, a background tab, a second window, the
needle-gone and disconnected paths, a client killed mid-request, and the
same run driven from WSL.
@github-actions

This comment has been minimized.

@lhecker Leonard Hecker (lhecker) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW if we plan to just upstream this existing (?) code, I'm fine with just noting down some nits. Otherwise, I have some ideas how to improve it, like the STA/MTA thing.

Comment thread src/tools/wtcli/main.cpp

// 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put these functions into src/types? We got CreateV5Uuid there already and we could link wtcli with it.

Unless you want to keep it portable of course.

Comment on lines +109 to +110
auto strong = get_strong();
co_await wil::resume_foreground(Dispatcher());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not possible to run the COM server on the STA main thread?


// 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 —

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah here's the answer. But ...long-running? What calls do we foresee to be computationally expensive?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants