How Windows Terminal Handles Tabs and Panes: A Deep Dive into the Architecture

Windows Terminal uses a hierarchical tree structure where each window contains a TerminalPage that owns multiple Tab objects, and each Tab manages a binary tree of Pane objects that can host terminal content or further splits.

The microsoft/terminal repository implements this architecture to support complex layouts with multiple terminals in a single window. Understanding how Windows Terminal handles tabs and panes reveals the design patterns behind its flexible UI, from simple single-pane tabs to nested grid layouts with dozens of terminals.

The Core Hierarchy: TerminalPage, Tab, and Pane

The architecture follows a strict three-level ownership model defined in the source code:


TerminalPage
   └─ Tab (multiple, stored in std::vector<TerminalApp::Tab> _tabs)
          └─ Pane (root) ──► child Pane ──► … (binary tree)
                 │
                 └─ TerminalPaneContent (TermControl)

TerminalPage (src/cascadia/TerminalApp/TerminalPage.h) serves as the top-level UI for each window. It maintains the tab collection in std::vector<TerminalApp::Tab> _tabs and provides methods like _CreateNewTabFlyout and _UpdateTabIndices to manage the tab strip.

Tab (src/cascadia/TerminalApp/Tab.h and Tab.cpp) acts as the logical container for a pane tree. Each Tab owns a _rootPane and tracks the _activePane. It exposes high-level operations including SplitPane, NavigateFocus, and UpdateTitle that delegate to the underlying pane tree.

Pane (src/cascadia/TerminalApp/Pane.h and Pane.cpp) represents a node in a binary split tree. A pane can be a leaf (holding content via TerminalPaneContent/TermControl) or a branch (containing _firstChild and _secondChild with a SplitState orientation). Key methods include _IsLeaf(), WalkTree, and NavigateDirection.

Tab Creation and Lifecycle

When a user opens a new tab via the + button, Ctrl+Shift+T, or the "New Tab" dropdown, TerminalPage::_CreateNewTabFromPane executes the creation sequence:

  1. Construct a root Pane containing the requested content (a new TermControl or settings pane).
  2. Build a Tab object wrapping this root pane.
  3. Register the tab with the TerminalPage and update the UI.
auto rootPane = make_shared<Pane>(content, true);
auto tabImpl = winrt::make_self<Tab>(rootPane);
auto tab = winrt::TerminalApp::Tab{ tabImpl };

The Tab constructor assigns unique IDs to every leaf pane, selects the first pane as active, registers UI events, and creates its TabViewItem via _MakeTabViewItem. The tab title is driven by the active pane's IPaneContent::Title() (see Tab::_GetActiveTitle).

The Binary Pane Tree and Splitting Logic

Pane splitting is the core mechanism enabling multiple terminals within a single tab. The operation is handled by Tab::SplitPane and implemented through the binary tree structure in Pane.

When a split is requested (e.g., Ctrl+Shift+- for horizontal):

  1. Walk the new pane tree and attach event handlers (_AttachEventHandlersToPane / _AttachEventHandlersToContent).
  2. Save the ID of the currently active pane (_activePane->Id()).
  3. Call Pane::Split on the active pane, which creates a new internal node with _firstChild and _secondChild and assigns the split direction (Horizontal or Vertical).
  4. The original active pane keeps its ID; the new pane receives a fresh ID from _nextPaneId.
  5. _UpdateActivePane is called with the newly created leaf pane so UI focus follows the split.
auto [original, newPane] = _activePane->Split(splitType, splitSize, pane);
_activePane = original;                     // keep focus on the original side
_UpdateActivePane(newPane);                 // give focus to the newly-added side

The split logic lives in Pane::_Split and uses SplitState to remember orientation. Size calculations (CalcSnappedDimension, PreCalculateCanSplit) ensure consistent behavior on high-DPI displays.

Focus Navigation and Pane Activation

Moving focus between panes uses directional navigation that respects the binary tree geometry. Tab::NavigateFocus forwards a focus direction (Up/Down/Left/Right) to the root pane, which recursively searches for the neighboring leaf:

if (const auto newFocus = _rootPane->NavigateDirection(_activePane, direction, _mruPanes))
{
    _changingActivePane = true;
    const auto res = _rootPane->FocusPane(newFocus);
    _changingActivePane = false;
    return res;
}

Pane::NavigateDirection uses PaneNeighborSearch (a helper struct) to locate the adjacent leaf based on split geometry. When a pane gains focus, its GotFocus event triggers Tab::_UpdateActivePane, which updates the UI (title, tab color, taskbar state) and fires Tab::ActivePaneChanged.

Advanced Pane Operations

Pane Swapping

Tab::SwapPane finds the neighbor pane in the requested direction and calls Pane::SwapPanes to exchange their positions in the tree without destroying content.

Resizing

Tab::ResizePane forwards a ResizeDirection (e.g., ResizeDirection::IncreaseWidth) to Pane::ResizePane, which moves the separator between two children by adjusting their relative sizes.

Zoom

Tab::ToggleZoom sets _zoomedPane and replaces the tab's content with the zoomed pane's Grid. Exiting zoom restores the original root pane layout.

All these operations rely on the binary pane-tree structure; they manipulate relationships between siblings rather than absolute screen coordinates.

Tab-Wide State Aggregation

A Tab aggregates information from all its leaf panes to present a unified UI:

  • TitleTab::_GetActiveTitle prefers a user-set runtime title, otherwise falls back to the active pane's content title via IPaneContent::Title().
  • Icon / Tab Color – Updated via Tab::_RecalculateAndApplyTabColor whenever any leaf fires TabColorChanged.
  • Taskbar ProgressTab::_UpdateProgressState collects each leaf's TaskbarState via Pane::CollectTaskbarStates and selects the most severe according to Windows API priority rules.
  • Bell Indicator – A TermControl raises BellRequested; the tab shows a visual bell and may flash the taskbar.

Closing and Detaching Panes

  • Closing a paneTab::ClosePane forwards to the active leaf's Close(). If the leaf is the only pane left, the Tab raises Closed, and TerminalPage removes the tab.
  • Detaching a paneTab::DetachPane removes the active leaf from the tree via Pane::DetachPane and promotes its sibling to fill the space.
  • Closing a tabTab::Close raises its Closed event; TerminalPage::_HandleCloseTabRequested removes the UI element and disposes the root pane.

Code Examples

Create a New Tab with a Custom Profile

using namespace winrt::Microsoft::Terminal::Settings::Model;

// Build the profile arguments
auto profile = Profile{};
profile.Guid(guid_of_my_profile);
auto args = NewTabArgs{ winrt::make<NewTerminalArgs>(profile) };

// Ask the action dispatcher to open the tab
winrt::Microsoft::Terminal::Control::ShortcutActionDispatch dispatch = …;
dispatch.DoAction(tab, ShortcutAction::NewTab, args);

Source: Tab::BuildStartupActions (creates the NewTab action) – see Tab.cpp line 55‑57.

Split the Active Pane Horizontally at 30 %

// In a key-binding handler (e.g. Ctrl+Shift+-)
winrt::Microsoft::Terminal::Settings::Model::SplitDirection direction = SplitDirection::Horizontal;
float splitSize = 0.3f;               // 30 % of the current pane width

tab.SplitPane(direction, splitSize, nullptr); // `nullptr` creates a fresh terminal pane

Source: Tab::SplitPaneTab.cpp lines 160‑168.

Move Focus to the Pane on the Right

tab.NavigateFocus(FocusDirection::Right);

Source: Tab::NavigateFocusTab.cpp lines 658‑666.

Zoom the Active Pane

tab.ToggleZoom();   // toggles between full-screen pane and normal layout

Source: Tab::ToggleZoom (delegates to Pane::Maximize / Pane::Restore) – see Tab.cpp around line 80.

Change the Tab’s Title from Code

tab.SetTabText(L"My Custom Title");

Source: Tab::SetTabTextTab.cpp line 73‑77.

Key Source Files

File Description
[TerminalPage.h](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/TerminalPage.h) Window-level container; creates tabs and handles tab-strip events.
[Tab.h](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/Tab.h) Declaration of the Tab class managing pane-tree state and UI actions.
[Tab.cpp](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/Tab.cpp) Full implementation of tab logic including creation, splitting, and focus management.
[Pane.h](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/Pane.h) Core split-tree node defining leaf vs. branch states and navigation helpers.
[Pane.cpp](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/Pane.cpp) Algorithms for splitting, resizing, zooming, and recursive tree traversal.
[TerminalPaneContent.h](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/TerminalPaneContent.h) Interface for leaf content hosting the actual terminal emulator (TermControl).
[TabRowControl.h](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/TabRowControl.h) XAML control rendering the visual tab header with title and close button.
[TaskbarState.h](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalApp/TaskbarState.h) Helper mapping per-pane progress states to Windows taskbar priority rules.

Summary

  • Windows Terminal handles tabs and panes through a strict three-tier hierarchy: TerminalPage manages the window, Tab owns a binary tree of Pane objects, and leaf panes host TerminalPaneContent.
  • Pane splitting creates a binary tree structure where each internal node stores _firstChild and _secondChild, enabling recursive navigation and resizing without absolute coordinate tracking.
  • Focus management uses directional navigation (NavigateFocus) that traverses the pane tree to find geometric neighbors based on split orientation.
  • State aggregation happens at the tab level, where Tab collects title, color, progress, and bell events from all leaf panes to update the UI and Windows taskbar.
  • Key implementation files are located in src/cascadia/TerminalApp/, with Tab.cpp and Pane.cpp containing the core logic for layout management.

Frequently Asked Questions

How does Windows Terminal store the relationship between split panes?

Windows Terminal stores split panes as a binary tree where each Pane object acts as a node. If a pane is split, it becomes a branch node containing std::shared_ptr<Pane> _firstChild and _secondChild along with a SplitState indicating horizontal or vertical orientation. Leaf nodes host the actual terminal content via TerminalPaneContent. This tree structure allows the application to perform recursive operations like navigation, resizing, and zooming without tracking absolute screen coordinates.

What happens when I close a pane that has been split?

When you close a pane, Tab::ClosePane forwards the request to the active leaf's Close() method. If the closed pane is the last leaf in its branch, the parent pane is removed from the tree and its sibling pane is promoted to fill the space via Pane::DetachPane. If the closed pane was the only pane remaining in the tab, the Tab raises a Closed event, causing TerminalPage to remove the entire tab from the window.

How does focus navigation work between panes?

Focus navigation uses geometric direction rather than tab order. When you press Alt+ArrowKey, Tab::NavigateFocus calls Pane::NavigateDirection on the root pane with the current active pane and direction (Up, Down, Left, or Right). The method uses a PaneNeighborSearch helper to recursively traverse the binary tree and find the leaf pane that is geometrically adjacent in the requested direction based on the split orientations stored in SplitState. Once found, Tab::_UpdateActivePane updates the UI state and fires the ActivePaneChanged event.

Can I programmatically control pane zoom and resizing?

Yes, the Tab class exposes methods to control zoom and resizing programmatically. Calling Tab::ToggleZoom() sets _zoomedPane and replaces the tab's content with the zoomed pane's Grid, delegating to Pane::Maximize and Pane::Restore to handle the layout switch. For resizing, Tab::ResizePane accepts a ResizeDirection (such as IncreaseWidth) and forwards it to Pane::ResizePane, which adjusts the separator between two child panes by modifying their relative sizes in the binary tree.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →