AgentCLIHub Development Notes 1: Bringing Multiple AI CLIs into One Window with ConPTY and xterm.js
AgentCLIHub Development Notes · 01 / 10

Using several AI CLIs means more terminal windows and working directories to manage. Bringing them into one window requires preserving each CLI's interactive behavior, including approval prompts, slash commands, and cursor movement. Agent CLI Hub does this by placing a C# host between Windows ConPTY and the web-based terminal xterm.js.
1. An execution environment that preserves interactive interfaces
Running a command and reading its standard output is relatively straightforward. But an interactive CLI outputs more than text. It also sends control sequences that move the cursor, change colors, and update areas already drawn on screen. Arrow keys and Ctrl combinations must reach the CLI as well. Turning this stream into line-by-line logs loses the meaning of the terminal display.
Agent CLI Hub's ConPty creates a Windows pseudoconsole and attaches a child process to it. The host reads the bytes emitted by the CLI, and xterm.js interprets them to render the terminal. This reduces the need to recreate each CLI's conversational interface and lets the app preserve its existing controls.
There are trade-offs. Individual messages are harder to treat as separate cards, as they would be in a typical web chat, and the screen dimensions and terminal state must stay in sync. In return, the app needs less reimplementation whenever a CLI adds an approval prompt or a new interface element. ConPTY implementation
2. Four parts with distinct responsibilities
The responsibilities between execution and display are divided as follows:
CliProfile: configures the launch command and options for each CLIConPty: manages the pseudoconsole, pipes, process, and Windows handlesTerminalHost: handles startup, input, output, and resizing for one tabweb/index.html: creates xterm.js instances and displays the terminals
WPF's MainWindow receives messages from WebView2 and routes them to the appropriate tab's host. In the other direction, the host creates JSON messages and passes them to WebView2. The web interface loads from the local web folder through a virtual host mapping, and xterm.js is bundled with the app. Displaying the interface does not require downloading the terminal scripts from a CDN.
The key to this separation is the reason each part changes. Changes to Windows process handling belong in ConPty; changes to a particular CLI's flags belong in CliProfile. Changing the appearance of the tabs should not require editing executable discovery logic. WebView2 initialization and message routing
3. Tab IDs keep input and output together
When several CLIs run at once, the first requirement is keeping each conversation's input and output separate. Agent CLI Hub creates one TerminalHost and one xterm instance per tab. Each host retains the working directory and CLI information supplied when it was created.
When the user types in the web interface, a term.in message carries the tab ID and input string. C# looks up the host by ID and sends the input only to that host's ConPTY. Output returns in term.out messages, and the interface writes it only to the xterm instance with the matching ID. Messages for closed or unknown tabs are not redirected to another tab.
The host's Send method adds the ID to every terminal message. This reduces the chance of forgetting it in separate output, exit, or error handlers. It also keeps the working directory and output destination tied to their tab, even when two projects are open in the same window. Per-tab hosts and message sending, Adding the tab ID
4. Terminal dimensions are data that must travel too
A terminal's column and row counts affect how a CLI lays out its interface. After attaching xterm.js to the page, the app uses FitAddon to calculate its dimensions and includes the column and row counts in the initial term.start message. Later changes to xterm's size send a term.resize message, which ultimately calls Windows' ResizePseudoConsole.
Intermediate sizes produced while dragging the window are debounced with a 200 ms delay. This avoids sending a stream of unnecessary changes to a CLI that redraws its layout on each resize. There is also a path to synchronize the dimensions again after startup, since the initial size can differ from the size after the interface finishes laying out.
Key handling preserves the CLI's own interpretation wherever possible. For example, Ctrl+C copies text when there is a selection; otherwise, it remains terminal input. This small distinction keeps the copy feature from intercepting the interrupt key. Creating xterm instances and wiring up events, Resize handling
5. Handling byte boundaries and resource lifetimes
Reads from ConPTY return byte arrays, not strings. A UTF-8 character can span two reads, so converting each chunk independently to a string can corrupt characters. TerminalHost uses a stateful UTF-8 Decoder to combine incomplete bytes from the previous chunk with the next one.
The types used at the Windows API boundary must also be explicit. CreateProcessW uses Unicode settings, and its mutable command-line argument is passed as a StringBuilder. The pseudoconsole handle is added to the process startup attributes. Creating the pseudoconsole and attaching the child process to it are separate steps.
During shutdown, the app closes the pseudoconsole, waits for the process to exit, terminates it if necessary, and releases the remaining handles and attribute list. For an app intended to run for long periods, closing a tab must also manage the lifetimes of its process and pipes. Output decoding, ConPTY shutdown
Key takeaways
- ConPTY provides the terminal environment the CLI expects, while xterm.js interprets its output for display.
- Tab IDs connect working directories, processes, input, and output.
- Screen dimensions, UTF-8 boundaries, and process shutdown are part of an integrated terminal's basic behavior.
- Separating CLI-specific differences from shared terminal handling narrows the scope of changes.
Reviewed source at a pinned commit
Reference: repository snapshot at commit 46dfca2
Project: AgentCLIHub GitHub repository
댓글
댓글 쓰기