agentleFS
MicrosoftKnown publishermicrosoft/microsoft-ui-reactor/plugins/reactor/skills/reactor-docking/SKILL.md

reactor-docking

Skill839 starsChanged 6 days agoMITView on GitHub

Reactor docking windows — `DockManager`, `DockSplit`/`DockTabGroup`/`Document`/`ToolWindow`, drag-to-float/redock, roles (`DocumentArea`/`ToolWindowStrip`), persistence. Use when building IDE-/Office-shaped layouts with dockable, tear-out tool windows and document wells. READ THIS BEFORE wiring `DockManager` — the content-vs-shape ownership model is the #1 thing people get wrong.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Do NOT round-trip the host's live layout back into your own state:

    line 33
  2. Very specific:

    Add/remove a pane ⇒ the Key set changes ⇒ the host picks it up.

    line 86
  3. Very specific:

    Every pane needs a stable, equatable Key — it's how the host preserves pane state across reorders, tear-out, and re-dock.

    line 123

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Docking in Reactor

DockManager hosts a tree of dockable panes that the user can drag, split, tear out into floating windows, re-dock, pin to a side, and persist. It is the foundation for IDE-/Office-shaped apps (solution explorer + editor well + tool panes).

configure: host => DockingNativeInterop.Register(host.Reconciler)  // required once at startup

The ownership model — READ THIS FIRST

This is the single most important rule, and the easiest to get wrong:

The app owns CONTENT. The host owns SHAPE.

  • Content = which panes exist, their Title/Content/Key, which document is active. The app declares this via manager.Layout, fresh every render, derived from its own UseState.
  • Shape = the user's drag-modified arrangement: split orientations/ratios, which group a tab lives in, floating windows. The host owns this internally (spec 045 §2.30) and resolves the effective layout each render by matching your content to its shape by Key.

❌ The anti-pattern that breaks everything

Do NOT round-trip the host's live layout back into your own state:

// ❌ WRONG — feeds the host's shape back into the content prop.
var (layout, setLayout) = UseState<DockNode?>(BuildLayout());
new DockManager {
    Layout = layout,
    OnLiveLayoutChanged = next => setLayout(next),   // ⛔ double-owns the shape
};

This double-owns the shape. Symptoms (all of these at once):

  • Drag-out-to-float works, but you can't drag back to re-dock.
  • Clicking tabs doesn't switch / selection resets.
  • Splitter drags snap back.

OnLiveLayoutChanged is for observation only (e.g. a layout inspector). It is never a setter for manager.Layout.

✅ The correct pattern

The app holds which documents are open (content), not the live tree. Opening or closing a pane changes the set of Keys in manager.Layout; the host detects the key-set change and merges it into its shape. Reset is a .WithKey(...) remount.

var (openDocs, setOpenDocs) = UseState(ImmutableList.Create(File.AppCs));
var (activeKey, setActiveKey) = UseState<object?>(File.AppCs.Key);
var (epoch, bumpEpoch) = UseReducer(0);

void Open(ProjectFile f) {
    if (!openDocs.Any(d => Equals(d.Key, f.Key))) setOpenDocs(openDocs.Add(f));
    setActiveKey(f.Key);                       // focus it (drives SelectedIndex)
}

var editorWell = new DockTabGroup(
    openDocs.Select(MakeDoc).ToArray(),
    SelectedIndex: openDocs.FindIndex(d => Equals(d.Key, activeKey)),
    ShowWhenEmpty: true,
    Role: DockGroupRole.DocumentArea);

new DockManager {
    Layout = BuildLayout(editorWell),
    // Sync state when the host closes a tab via its X button. The host already
    // removed it from its shape; drop it from openDocs so the key sets converge.
    OnDocumentClosing = args =>
        setOpenDocs(openDocs.RemoveAll(d => Equals(d.Key, args.Document.Key))),
}.WithKey($"dock-{epoch}");   // View ▸ Reset Layout: bumpEpoch + reset openDocs

Rules of thumb:

  • Derive manager.Layout from app state every render. Never store the host's tree.
  • Add/remove a pane ⇒ the Key set changes ⇒ the host picks it up.
  • Model-mutator additions (drag, DockHostModel.Dock) keep the same app key set, so the host preserves them. That's why round-tripping is both unnecessary and harmful.
  • Reset / discard drag state ⇒ .WithKey($"dock-{epoch}") bump remounts the host.
  • Layout persistence across launches ⇒ PersistenceId, not OnLiveLayoutChanged.

Controlled prop note: SelectedIndex-style element properties are Optional<int> under the hood. Factory parameters such as SelectedIndex: still accept int, but if you read an element record directly use .Value or .GetValueOrDefault(-1). See migration/050-optional-t.md.

Building a layout

new DockSplit(Orientation.Vertical, new DockNode[] {
    new DockSplit(Orientation.Horizontal, new DockNode[] {
        new DockTabGroup(new DockableContent[]{ solutionTool }, Width: 260,
            Role: DockGroupRole.ToolWindowStrip),
        editorWell,                                              // DocumentArea
        new DockTabGroup(new DockableContent[]{ propsTool, gitTool }, Width: 300,
            TabPosition: TabPosition.Bottom, CompactTabs: true,
            Role: DockGroupRole.ToolWindowStrip),
    }),
    new DockTabGroup(new DockableContent[]{ output, terminal, errors },
        Height: 220, TabPosition: TabPosition.Bottom, CompactTabs: true,
        Role: DockGroupRole.ToolWindowStrip),
});
  • Document (closable, not pinnable) vs ToolWindow (hideable, side-pinnable, AllowedSides mask). Both reconcile through DockableContent.
  • Roles (spec 046): DocumentArea is the preferred Dock(Center) target and survives empty (the well stays a visible drop target). ToolWindowStrip is the preferred target for tool-window drops. Use object-initializer syntax for Document/ToolWindow so you opt into permission flags additively.
  • Every pane needs a stable, equatable Key — it's how the host preserves pane state across reorders, tear-out, and re-dock. No fallback to title-keying.

Pane content gotchas

Make pane bodies fill the pane

A docked pane body is content-sized by default — a plain Border/TextBox collapses to its desired height at the top of the pane. To fill, put it in a flex container with grow:

FlexColumn(
    editorTextBox.Flex(grow: 1, basis: 0)
).Flex(grow: 1);

Multi-line TextBox: AcceptsReturn must be an element prop, not .Set

The TextBox descriptor applies AcceptsReturn/TextWrapping before Text on purpose — single-line mode truncates Text at the first newline. A .Set(tb => tb.AcceptsReturn = true) lambda runs after Text is assigned, so the body collapses to one line. Use the first-class modifiers:

// ✅ ordered before Text by the descriptor
TextBox(text, setText).AcceptsReturn().TextWrapping(TextWrapping.NoWrap)

// ❌ runs after Text → multi-line content truncated to the first line
TextBox(text, setText).Set(tb => tb.AcceptsReturn = true)

Generally: any prop whose ordering relative to another prop matters belongs on the element (.AcceptsReturn(), .TextWrapping()), not in a .Set(...) escape hatch — .Set always runs last.

Key APIs

APIPurpose
DockManager { Layout, PersistenceId, ... }The host element
DockSplit(Orientation, children)Resizable split (ratios owned by host)
DockTabGroup(docs, TabPosition, CompactTabs, SelectedIndex, Width/Height, Role)A tab group
Document { Title, Key, Content, CanClose }Closable document pane
ToolWindow { Title, Key, Content, CanPin, AllowedSides, ... }Hideable/pinnable tool pane
.WithKey($"dock-{epoch}")Remount to reset/discard drag shape
OnDocumentClosingSync app state when the host closes a tab
OnLiveLayoutChangedObserve the resolved layout (never feed back to Layout)
DockingNativeInterop.Register(host.Reconciler)One-time startup registration

See samples/apps/reactor-ide for a complete IDE-shaped app exercising all of this.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.