Documentation
API: core (@fragiola/dockable)

Commands

Every command the model runs, with its payload and result types and how to run it from code or as JSON. Generated from the registry.

The model runs 25 commands. Each one takes a JSON payload, checked against its JSON Schema, and returns a CommandResult: { ok: true, value }, or { ok: false, error }. Run one with model.run(name, payload) (typed by your registry), or give it as untrusted JSON to model.dispatch({ command, payload }), which validates it first. model.get("commands") returns this same list with the schemas, ready to become tool definitions for an assistant.

Every command can return invalid_payload (the payload does not match its schema, with the path of each problem), vetoed (a middleware refused it), queued (a middleware ran it while another command was running) and middleware_error; each command lists the ones its own rules add: not_found (a node it names is missing) and refused (a rule of the layout refuses the change). See the command bus for the results, the middleware and the events.

The payload types below use T, your registry (see typed data): a tab's data is checked against its component.

Tabs

tab.add

Add a new tab. component names what the tab shows, label is its name (the app renders it) and data holds its state (validated when the app registered a data schema). to is a tabset, a row, a border or a layout id; location center adds it to that tabset or border at index (-1 appends), an edge of a tabset splits it, an edge of a root row docks the tab to that side of the layout.

Payload (TabAddPayload<T>)

fieldtyperequireddescription
idstringnothe new tab's id (generated when omitted)
componentstringyeswhat the tab shows (a key of the app's registry)
labelstringyesthe tab's name (the app renders it; the packages never do)
dataJSONnothe app's data (any JSON value)
pinnedbooleannoa pinned tab sits at the start of its strip, cannot close and cannot leave its tabset
closablebooleannowhether the tab can be closed
draggablebooleannowhether the tab can be dragged
poppablebooleannowhether the tab can be popped out into a window
renamablebooleannowhether the tab can be renamed (tab.rename)
minWidthnumbernothe smallest width, in px
minHeightnumbernothe smallest height, in px
maxWidthnumbernothe largest width, in px
maxHeightnumbernothe largest height, in px
borderWidthnumbernoits panel's width in a start or end border, in px
borderHeightnumbernoits panel's height in a top or bottom border, in px
tostringyesa tabset, a row, a border, or a layout id (its root row)
location"center" | "top" | "bottom" | "start" | "end"nocenter (default) goes into the target; an edge of a tabset splits it; an edge of a row docks beside its children
indexintegernofor a center drop: the insertion index; -1 appends
selectbooleannowhether the tab is selected in its new place

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, refused, besides those every command can return.

model.run("tab.add", {
    component: "editor",
    label: "a.ts",
    data: {
        path: "/a.ts"
    },
    to: "tabset-1"
});
{ "command": "tab.add", "payload": {"component":"editor","label":"a.ts","data":{"path":"/a.ts"},"to":"tabset-1"} }

tab.select

Select a tab, making it visible. In a tabset the tabset also becomes the active one; in a border the border's panel opens. Selecting the selected tab changes nothing.

Payload ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, besides those every command can return.

model.run("tab.select", {
    tabId: "tab-1"
});
{ "command": "tab.select", "payload": {"tabId":"tab-1"} }

tab.close

Close a tab and remove it from the layout. Refused for a pinned tab or one that is not closable.

Payload ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, refused, besides those every command can return.

model.run("tab.close", {
    tabId: "tab-1"
});
{ "command": "tab.close", "payload": {"tabId":"tab-1"} }

tab.move

Move a tab to another place: into a tabset or border at an index (location center), beside a tabset (an edge location splits it), or to an edge of a layout (to a root row or a layout id, with an edge location). Refused where the tab or the target does not allow it.

Payload (TabMovePayload)

fieldtyperequireddescription
tabIdstringyesthe tab's id
tostringyesa tabset, a row, a border, or a layout id (its root row)
location"center" | "top" | "bottom" | "start" | "end"nocenter (default) goes into the target; an edge of a tabset splits it; an edge of a row docks beside its children
indexintegernofor a center drop: the insertion index; -1 appends
selectbooleannowhether the tab is selected in its new place

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, refused, besides those every command can return.

model.run("tab.move", {
    tabId: "tab-1",
    to: "tabset-2",
    location: "end"
});
{ "command": "tab.move", "payload": {"tabId":"tab-1","to":"tabset-2","location":"end"} }

tab.set-data

Change some of a tab's data: data is a shallow patch, whose top-level keys replace the tab's while the others stay (none is removed). The merged data is validated when the app registered a schema for the tab's component. To switch the component, use tab.set-component.

Payload (TabSetDataPayload<T>)

fieldtyperequireddescription
tabIdstringyesthe tab's id
dataobjectyesthe keys to change; each replaces the tab's own (a shallow merge)

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, besides those every command can return.

model.run("tab.set-data", {
    tabId: "tab-1",
    data: {
        dirty: true
    }
});
{ "command": "tab.set-data", "payload": {"tabId":"tab-1","data":{"dirty":true}} }

tab.set-component

Switch a tab to another component (or reset it to its own): data is the component's whole new value, validated when the app registered a schema for that component. The tab keeps its id, label and place.

Payload (TabSetComponentPayload<T>)

fieldtyperequireddescription
tabIdstringyesthe tab's id
componentstringyesthe tab's new component (a key of the app's registry)
dataJSONnothe component's whole data

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, besides those every command can return.

model.run("tab.set-component", {
    tabId: "tab-1",
    component: "editor",
    data: {
        path: "/b.ts"
    }
});
{ "command": "tab.set-component", "payload": {"tabId":"tab-1","component":"editor","data":{"path":"/b.ts"}} }

tab.pin

Pin (value true) or unpin a tab of a tabset. Pinned tabs sit at the start of the strip, cannot be closed and cannot be dragged out of their tabset.

Payload ({ tabId: string; value: boolean })

fieldtyperequireddescription
tabIdstringyesthe tab's id
valuebooleanyestrue pins, false unpins

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, refused, besides those every command can return.

model.run("tab.pin", {
    tabId: "tab-1",
    value: true
});
{ "command": "tab.pin", "payload": {"tabId":"tab-1","value":true} }

tab.popout

Open a tab in a new browser window (a window layout). rect is the window's screen rect; a default is used without one. Refused when the tab does not allow popouts, is pinned or is already in a window.

Payload ({ tabId: string; rect?: Rect })

fieldtyperequireddescription
tabIdstringyesthe tab's id
rectobjectnothe window's screen rect; without one, a 600x400 window offset 50px per open window (engine.popout passes the tab's place on screen)

Result ({ windowId: string })

fieldtyperequireddescription
windowIdstringyesthe new window's id

Errors: not_found, refused, besides those every command can return.

model.run("tab.popout", {
    tabId: "tab-1"
});
{ "command": "tab.popout", "payload": {"tabId":"tab-1"} }

tab.rename

Change a tab's label. Refused for a tab that is not renamable (its own renamable, else the layout default).

Payload ({ tabId: string; label: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id
labelstringyesthe tab's new name

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, refused, besides those every command can return.

model.run("tab.rename", {
    tabId: "tab-1",
    label: "b.ts"
});
{ "command": "tab.rename", "payload": {"tabId":"tab-1","label":"b.ts"} }

tab.configure

Change a tab's behaviour flags and size limits. Absent keys are left as they are. A null flag or limit removes the tab's own value so the layout default applies. The label changes with tab.rename.

Payload (TabConfigurePayload)

fieldtyperequireddescription
tabIdstringyesthe tab's id
closableboolean | nullnowhether the tab can be closed (null removes it: the layout default applies)
draggableboolean | nullnowhether the tab can be dragged (null removes it: the layout default applies)
poppableboolean | nullnowhether the tab can be popped out into a window (null removes it: the layout default applies)
renamableboolean | nullnowhether the tab can be renamed (tab.rename) (null removes it: the layout default applies)
minWidthnumber | nullnothe smallest width, in px (null removes it: the layout default applies)
minHeightnumber | nullnothe smallest height, in px (null removes it: the layout default applies)
maxWidthnumber | nullnothe largest width, in px (null removes it: the layout default applies)
maxHeightnumber | nullnothe largest height, in px (null removes it: the layout default applies)
borderWidthnumber | nullnoits panel's width in a start or end border, in px (null removes it: the layout default applies)
borderHeightnumber | nullnoits panel's height in a top or bottom border, in px (null removes it: the layout default applies)

Result ({ tabId: string })

fieldtyperequireddescription
tabIdstringyesthe tab's id

Errors: not_found, besides those every command can return.

model.run("tab.configure", {
    tabId: "tab-1",
    closable: false
});
{ "command": "tab.configure", "payload": {"tabId":"tab-1","closable":false} }

Tabsets

tabset.activate

Make a tabset the active one of its layout.

Payload ({ tabsetId: string })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id

Result ({ tabsetId: string })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id

Errors: not_found, besides those every command can return.

model.run("tabset.activate", {
    tabsetId: "tabset-1"
});
{ "command": "tabset.activate", "payload": {"tabsetId":"tabset-1"} }

tabset.maximize

Maximize a tabset so it fills its layout (value true), or restore it (value false). Maximizing also makes it active. Refused when the tabset does not allow it or is the only tabset of its layout.

Payload ({ tabsetId: string; value: boolean })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id
valuebooleanyestrue maximizes, false restores

Result ({ tabsetId: string })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id

Errors: not_found, refused, besides those every command can return.

model.run("tabset.maximize", {
    tabsetId: "tabset-1",
    value: true
});
{ "command": "tabset.maximize", "payload": {"tabsetId":"tabset-1","value":true} }

tabset.close

Close a tabset: its closable tabs close, and the tabset is removed once empty. Refused when the tabset is not closable.

Payload ({ tabsetId: string })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id

Result ({ closedTabIds: string[] })

fieldtyperequireddescription
closedTabIdsstring[]yesthe ids of the tabs that closed

Errors: not_found, refused, besides those every command can return.

model.run("tabset.close", {
    tabsetId: "tabset-1"
});
{ "command": "tabset.close", "payload": {"tabsetId":"tabset-1"} }

tabset.move

Move a whole tabset: merge its tabs into another tabset (location center), place it beside a tabset (an edge), or dock it to an edge of a layout.

Payload (TabsetMovePayload)

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id
tostringyesa tabset, a row, or a layout id (its root row)
location"center" | "top" | "bottom" | "start" | "end"nocenter (default) merges its tabs into the target; an edge of a tabset places it beside; an edge of a row docks it there
indexintegernofor a merge: where its tabs go; -1 appends

Result ({ tabsetId: string })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id

Errors: not_found, refused, besides those every command can return.

model.run("tabset.move", {
    tabsetId: "tabset-1",
    to: "main",
    location: "bottom"
});
{ "command": "tabset.move", "payload": {"tabsetId":"tabset-1","to":"main","location":"bottom"} }

tabset.popout

Open a whole tabset in a new browser window. Refused when any of its tabs does not allow popouts, when it is empty, or when it is already in a window.

Payload ({ tabsetId: string; rect?: Rect })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id
rectobjectnothe window's screen rect; without one, a 600x400 window offset 50px per open window (engine.popout passes the tabset's place on screen)

Result ({ windowId: string })

fieldtyperequireddescription
windowIdstringyesthe new window's id

Errors: not_found, refused, besides those every command can return.

model.run("tabset.popout", {
    tabsetId: "tabset-1"
});
{ "command": "tabset.popout", "payload": {"tabsetId":"tabset-1"} }

tabset.configure

Change a tabset's behaviour flags, size limits or data. A null value removes the tabset's own value so the layout default applies (data: null removes the data).

Payload (TabsetConfigurePayload<T>)

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id
droppableboolean | nullnowhether tabs can be dropped into it (null removes it: the layout default applies)
draggableboolean | nullnowhether the whole tabset can be dragged (null removes it: the layout default applies)
splittableboolean | nullnowhether a drop on one of its edges can split it (null removes it: the layout default applies)
maximizableboolean | nullnowhether it can be maximized (null removes it: the layout default applies)
closableboolean | nullnowhether it can be closed (null removes it: the layout default applies)
deleteWhenEmptyboolean | nullnowhether it is removed when its last tab leaves (null removes it: the layout default applies)
autoSelectTabboolean | nullnowhether a tab added to it is selected (null removes it: the layout default applies)
minWidthnumber | nullnothe smallest width, in px (null removes it: the layout default applies)
minHeightnumber | nullnothe smallest height, in px (null removes it: the layout default applies)
maxWidthnumber | nullnothe largest width, in px (null removes it: the layout default applies)
maxHeightnumber | nullnothe largest height, in px (null removes it: the layout default applies)
dataJSONnothe app's data (any JSON value)

Result ({ tabsetId: string })

fieldtyperequireddescription
tabsetIdstringyesthe tabset's id

Errors: not_found, besides those every command can return.

model.run("tabset.configure", {
    tabsetId: "tabset-1",
    maximizable: false
});
{ "command": "tabset.configure", "payload": {"tabsetId":"tabset-1","maximizable":false} }

Rows

row.resize

Set the relative weights of a row's children, one positive number per child in order (the splitters issue this while dragged).

It may run as a step of a continuous gesture: model.run(…, { transient: true }).

Payload ({ rowId: string; weights: number[] })

fieldtyperequireddescription
rowIdstringyesthe row's id
weightsnumber[]yesone positive weight per child, in order

Result ({ rowId: string })

fieldtyperequireddescription
rowIdstringyesthe row's id

Errors: not_found, besides those every command can return.

model.run("row.resize", {
    rowId: "row-1",
    weights: [
        30,
        70
    ]
});
{ "command": "row.resize", "payload": {"rowId":"row-1","weights":[30,70]} }

row.configure

Set (or, with null, remove) a row's data.

Payload (RowConfigurePayload<T>)

fieldtyperequireddescription
rowIdstringyesthe row's id
dataJSONnothe app's data (any JSON value)

Result ({ rowId: string })

fieldtyperequireddescription
rowIdstringyesthe row's id

Errors: not_found, besides those every command can return.

model.run("row.configure", {
    rowId: "row-1",
    data: {
        name: "Editors"
    }
});
{ "command": "row.configure", "payload": {"rowId":"row-1","data":{"name":"Editors"}} }

Borders

border.resize

Set the size in px of a border's panel (of its selected tab when that tab has its own border size). The size is clamped to the border's min and max.

It may run as a step of a continuous gesture: model.run(…, { transient: true }).

Payload ({ borderId: string; size: number })

fieldtyperequireddescription
borderIdstringyesthe border's id (border_<location> unless the layout names it)
sizenumberyesthe panel's new size, in px (kept within its limits)

Result ({ borderId: string; size: number })

fieldtyperequireddescription
borderIdstringyesthe border's id
sizenumberyesthe size it got, in px

Errors: not_found, besides those every command can return.

model.run("border.resize", {
    borderId: "border_start",
    size: 240
});
{ "command": "border.resize", "payload": {"borderId":"border_start","size":240} }

border.configure

Open or close a border's panel (open), switch it between docked and overlay (mode), show or hide it, or change its sizes, flags or data. Opening selects its first tab when none is selected. A null value removes the border's own value so the layout default applies.

Payload (BorderConfigurePayload<T>)

fieldtyperequireddescription
borderIdstringyesthe border's id (border_<location> unless the layout names it)
openbooleannoopen (true) or close (false) the border's panel
showboolean | nullnofalse hides the border entirely (null removes it: the layout default applies)
sizenumber | nullnoits panel's size, in px (null removes it: the layout default applies)
minSizenumber | nullnoits panel's smallest size, in px (null removes it: the layout default applies)
maxSizenumber | nullnoits panel's largest size, in px (null removes it: the layout default applies)
mode"docked" | "overlay" | nullnodocked (beside the layout) or overlay (over it) (null removes it: the layout default applies)
autoHideboolean | nullnohide the strip while the border has no tabs (a drag near its edge reveals it) (null removes it: the layout default applies)
droppableboolean | nullnowhether tabs can be dropped into it (null removes it: the layout default applies)
autoSelectTabWhenOpenboolean | nullnowhether a tab added while its panel is open is selected (null removes it: the layout default applies)
autoSelectTabWhenClosedboolean | nullnowhether a tab added while its panel is closed is selected (which opens it) (null removes it: the layout default applies)
dataJSONnothe app's data (any JSON value)

Result ({ borderId: string })

fieldtyperequireddescription
borderIdstringyesthe border's id

Errors: not_found, refused, besides those every command can return.

model.run("border.configure", {
    borderId: "border_start",
    open: true
});
{ "command": "border.configure", "payload": {"borderId":"border_start","open":true} }

Windows

window.close

Close a popout window layout: its tabs move back into the main layout's active tabset (its first tabset when none is active), and the window closes.

Payload ({ windowId: string })

fieldtyperequireddescription
windowIdstringyesthe window layout's id

Result ({ tabIds: string[] })

fieldtyperequireddescription
tabIdsstring[]yesthe ids of the tabs moved back into the main layout

Errors: not_found, besides those every command can return.

model.run("window.close", {
    windowId: "window-1"
});
{ "command": "window.close", "payload": {"windowId":"window-1"} }

window.configure

Record a popout window's screen rect (the engine does this when the window moves or resizes, so a saved layout reopens it in place).

It may run as a step of a continuous gesture: model.run(…, { transient: true }).

Payload ({ windowId: string; rect: Rect })

fieldtyperequireddescription
windowIdstringyesthe window layout's id
rectobjectyesthe window's screen rect

Result ({ windowId: string })

fieldtyperequireddescription
windowIdstringyesthe window's id

Errors: not_found, besides those every command can return.

model.run("window.configure", {
    windowId: "window-1",
    rect: {
        x: 100,
        y: 80,
        width: 800,
        height: 600
    }
});
{ "command": "window.configure", "payload": {"windowId":"window-1","rect":{"x":100,"y":80,"width":800,"height":600}} }

The layout

layout.configure

Change the layout defaults: the default behaviour of tabs, tabsets and borders, and layout settings (root orientation, edge docking). Fields are merged; a null value removes one.

Payload ({ defaults: LayoutDefaultsPatch })

fieldtyperequireddescription
defaultsobjectyesthe defaults to change, by kind (tab, tabset, border, layout)

Result (Record<never, never>)

No fields.

Errors: only those every command can return.

model.run("layout.configure", {
    defaults: {
        tab: {
            closable: false
        }
    }
});
{ "command": "layout.configure", "payload": {"defaults":{"tab":{"closable":false}}} }

layout.load

Replace the whole layout with a JSON v1 document (a saved layout, an undo step, a remote sync). Nodes keep their identity by id, so tabs whose ids survive keep their mounted content.

Payload ({ layout: LayoutJson<T> })

fieldtyperequireddescription
layoutlayoutyesa layout document (JSON v1)

Result ({ addedNodeIds: string[]; removedNodeIds: string[] })

fieldtyperequireddescription
addedNodeIdsstring[]yesthe ids of the nodes only in the new layout
removedNodeIdsstring[]yesthe ids of the nodes only in the old layout

Errors: only those every command can return.

model.run("layout.load", {
    layout: {
        version: 1,
        root: {
            type: "row",
            children: [
                {
                    type: "tabset",
                    children: [
                        {
                            component: "editor",
                            label: "a.ts",
                            data: {
                                path: "/a.ts"
                            }
                        }
                    ]
                }
            ]
        }
    }
});
{ "command": "layout.load", "payload": {"layout":{"version":1,"root":{"type":"row","children":[{"type":"tabset","children":[{"component":"editor","label":"a.ts","data":{"path":"/a.ts"}}]}]}}} }

Batches

batch

Run several commands in order as one atomic step: if any fails, none applies. Emits one change event. Nested batches are flattened.

It may run as a step of a continuous gesture: model.run(…, { transient: true }).

Payload ({ commands: BatchEntry<T>[] })

fieldtyperequireddescription
commandsobject[]yesthe commands to run, in order

Result ({ results: unknown[] })

fieldtyperequireddescription
resultsJSON[]yeseach command's value, in order

Errors: those of the commands it runs (the first failure, at its path in commands); nothing applies unless all succeed.

model.run("batch", {
    commands: [
        {
            command: "tab.close",
            payload: {
                tabId: "tab-1"
            }
        },
        {
            command: "tab.close",
            payload: {
                tabId: "tab-2"
            }
        }
    ]
});
{ "command": "batch", "payload": {"commands":[{"command":"tab.close","payload":{"tabId":"tab-1"}},{"command":"tab.close","payload":{"tabId":"tab-2"}}]} }