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.
In this repository23 other files it gives its agents.
- AGENTS.mdAGENTS.md
- analyzer-dymSkill
- coverage-upliftSkill
- perf-compareSkill
- pr-lifecycleSkill
- pr-reviewSkill
- reactor-advancedSkill
- reactor-asyncSkill
- reactor-build-and-checkSkill
- reactor-chartsSkill
- reactor-commandingSkill
- reactor-designSkill
- reactor-devtoolsSkill
- reactor-dslSkill
- reactor-formsSkill
- reactor-getting-startedSkill
- reactor-inputSkill
- reactor-navigationSkill
- reactor-recipesSkill
- reactor-windowingSkill
- reactor-wrappingSkill
- reactor-appSkill
- ChartingSkill
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
- Distinctive: line 33
Do NOT round-trip the host's live layout back into your own state:
- Very specific: line 86
Add/remove a pane ⇒ the Key set changes ⇒ the host picks it up.
- Very specific: line 123
Every pane needs a stable, equatable Key — it's how the host preserves pane state across reorders, tear-out, and re-dock.
≈ “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.
Files
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 startupThe 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 viamanager.Layout, fresh every render, derived from its ownUseState. - 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 openDocsRules of thumb:
- Derive
manager.Layoutfrom app state every render. Never store the host's tree. - Add/remove a pane ⇒ the
Keyset 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, notOnLiveLayoutChanged.
Controlled prop note:
SelectedIndex-style element properties areOptional<int>under the hood. Factory parameters such asSelectedIndex:still acceptint, but if you read an element record directly use.Valueor.GetValueOrDefault(-1). Seemigration/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) vsToolWindow(hideable, side-pinnable,AllowedSidesmask). Both reconcile throughDockableContent.- Roles (spec 046):
DocumentAreais the preferredDock(Center)target and survives empty (the well stays a visible drop target).ToolWindowStripis the preferred target for tool-window drops. Use object-initializer syntax forDocument/ToolWindowso 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
| API | Purpose |
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 |
OnDocumentClosing | Sync app state when the host closes a tab |
OnLiveLayoutChanged | Observe 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.
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
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.

