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 23 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.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 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)
dataJSONnothe app's data (any JSON value)
pinnedbooleannoa pinned tab sits at the start of its strip, cannot close and cannot leave its tabset
enableClosebooleannowhether the tab can be closed
enableDragbooleannowhether the tab can be dragged
enablePopoutbooleannowhether the tab can be popped out into a window
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 left or right 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" | "left" | "right"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 ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

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

model.run("tab.add", {
    component: "editor",
    data: {
        name: "a.ts",
        path: "/a.ts"
    },
    to: "tabset-1"
});
{ "command": "tab.add", "payload": {"component":"editor","data":{"name":"a.ts","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 ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

Result ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

Errors: not_found, besides those every command can return.

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

tab.close

Close a tab and remove it from the layout. Refused for a pinned tab or one whose enableClose is false.

Payload ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

Result ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

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

model.run("tab.close", {
    tab: "tab-1"
});
{ "command": "tab.close", "payload": {"tab":"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
tabstringyesthe tab's id
tostringyesa tabset, a row, a border, or a layout id (its root row)
location"center" | "top" | "bottom" | "left" | "right"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 ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

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

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

tab.update

Replace a tab's data (and optionally switch its component). data is the whole new value, not a patch; it is validated when the app registered a schema for the component.

Payload (TabUpdatePayload<T>)

fieldtyperequireddescription
tabstringyesthe tab's id
componentstringyesthe tab's component (its current one to keep it)
dataJSONnothe app's data (any JSON value)

Result ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

Errors: not_found, besides those every command can return.

model.run("tab.update", {
    tab: "tab-1",
    component: "editor",
    data: {
        name: "b.ts",
        path: "/b.ts"
    }
});
{ "command": "tab.update", "payload": {"tab":"tab-1","component":"editor","data":{"name":"b.ts","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 ({ tab: string; value: boolean })

fieldtyperequireddescription
tabstringyesthe tab's id
valuebooleanyestrue pins, false unpins

Result ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

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

model.run("tab.pin", {
    tab: "tab-1",
    value: true
});
{ "command": "tab.pin", "payload": {"tab":"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 ({ tab: string; rect?: Rect })

fieldtyperequireddescription
tabstringyesthe 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 ({ window: string })

fieldtyperequireddescription
windowstringyesthe new window's id

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

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

tab.configure

Change a tab's behaviour flags and size limits. A null value removes the tab's own value so the layout default applies.

Payload (TabConfigurePayload)

fieldtyperequireddescription
tabstringyesthe tab's id
enableCloseboolean | nullnowhether the tab can be closed (null removes it: the layout default applies)
enableDragboolean | nullnowhether the tab can be dragged (null removes it: the layout default applies)
enablePopoutboolean | nullnowhether the tab can be popped out into a window (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 left or right 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 ({ tab: string })

fieldtyperequireddescription
tabstringyesthe tab's id

Errors: not_found, besides those every command can return.

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

Tabsets

tabset.activate

Make a tabset the active one of its layout.

Payload ({ tabset: string })

fieldtyperequireddescription
tabsetstringyesthe tabset's id

Result ({ tabset: string })

fieldtyperequireddescription
tabsetstringyesthe tabset's id

Errors: not_found, besides those every command can return.

model.run("tabset.activate", {
    tabset: "tabset-1"
});
{ "command": "tabset.activate", "payload": {"tabset":"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 ({ tabset: string; value: boolean })

fieldtyperequireddescription
tabsetstringyesthe tabset's id
valuebooleanyestrue maximizes, false restores

Result ({ tabset: string })

fieldtyperequireddescription
tabsetstringyesthe tabset's id

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

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

tabset.close

Close a tabset: its closable tabs close, and the tabset is removed once empty. Refused when the tabset's enableClose is false.

Payload ({ tabset: string })

fieldtyperequireddescription
tabsetstringyesthe tabset's id

Result ({ closed: string[] })

fieldtyperequireddescription
closedstring[]yesthe ids of the tabs that closed

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

model.run("tabset.close", {
    tabset: "tabset-1"
});
{ "command": "tabset.close", "payload": {"tabset":"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
tabsetstringyesthe tabset's id
tostringyesa tabset, a row, or a layout id (its root row)
location"center" | "top" | "bottom" | "left" | "right"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 ({ tabset: string })

fieldtyperequireddescription
tabsetstringyesthe tabset's id

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

model.run("tabset.move", {
    tabset: "tabset-1",
    to: "main",
    location: "bottom"
});
{ "command": "tabset.move", "payload": {"tabset":"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 ({ tabset: string; rect?: Rect })

fieldtyperequireddescription
tabsetstringyesthe 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 ({ window: string })

fieldtyperequireddescription
windowstringyesthe new window's id

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

model.run("tabset.popout", {
    tabset: "tabset-1"
});
{ "command": "tabset.popout", "payload": {"tabset":"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
tabsetstringyesthe tabset's id
enableDropboolean | nullnowhether tabs can be dropped into it (null removes it: the layout default applies)
enableDragboolean | nullnowhether the whole tabset can be dragged (null removes it: the layout default applies)
enableDivideboolean | nullnowhether a drop on one of its edges can split it (null removes it: the layout default applies)
enableMaximizeboolean | nullnowhether it can be maximized (null removes it: the layout default applies)
enableCloseboolean | 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 ({ tabset: string })

fieldtyperequireddescription
tabsetstringyesthe tabset's id

Errors: not_found, besides those every command can return.

model.run("tabset.configure", {
    tabset: "tabset-1",
    enableMaximize: false
});
{ "command": "tabset.configure", "payload": {"tabset":"tabset-1","enableMaximize":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 ({ row: string; weights: number[] })

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

Result ({ row: string })

fieldtyperequireddescription
rowstringyesthe row's id

Errors: not_found, besides those every command can return.

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

row.configure

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

Payload (RowConfigurePayload<T>)

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

Result ({ row: string })

fieldtyperequireddescription
rowstringyesthe row's id

Errors: not_found, besides those every command can return.

model.run("row.configure", {
    row: "row-1",
    data: {
        name: "Editors"
    }
});
{ "command": "row.configure", "payload": {"row":"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 ({ border: string; size: number })

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

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

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

Errors: not_found, besides those every command can return.

model.run("border.resize", {
    border: "border_left",
    size: 240
});
{ "command": "border.resize", "payload": {"border":"border_left","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
borderstringyesthe border's id (border_<location> unless the layout names it)
openbooleannoopen (true) or close (false) the border's panel
mode"docked" | "overlay" | nullnodocked (beside the layout) or overlay (over it) (null removes it: the layout default applies)
showboolean | nullnofalse hides the border entirely (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)
enableDropboolean | 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)
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)
dataJSONnothe app's data (any JSON value)

Result ({ border: string })

fieldtyperequireddescription
borderstringyesthe border's id

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

model.run("border.configure", {
    border: "border_left",
    open: true
});
{ "command": "border.configure", "payload": {"border":"border_left","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 ({ window: string })

fieldtyperequireddescription
windowstringyesthe window layout's id

Result ({ tabs: string[] })

fieldtyperequireddescription
tabsstring[]yesthe tabs moved back into the main layout

Errors: not_found, besides those every command can return.

model.run("window.close", {
    window: "window-1"
});
{ "command": "window.close", "payload": {"window":"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 ({ window: string; rect: Rect })

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

Result ({ window: string })

fieldtyperequireddescription
windowstringyesthe window's id

Errors: not_found, besides those every command can return.

model.run("window.configure", {
    window: "window-1",
    rect: {
        x: 100,
        y: 80,
        width: 800,
        height: 600
    }
});
{ "command": "window.configure", "payload": {"window":"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: {
            enableClose: false
        }
    }
});
{ "command": "layout.configure", "payload": {"defaults":{"tab":{"enableClose":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 ({ added: string[]; removed: string[] })

fieldtyperequireddescription
addedstring[]yesids only in the new layout
removedstring[]yesids 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",
                            data: {
                                name: "a.ts"
                            }
                        }
                    ]
                }
            ]
        }
    }
});
{ "command": "layout.load", "payload": {"layout":{"version":1,"root":{"type":"row","children":[{"type":"tabset","children":[{"component":"editor","data":{"name":"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: {
                tab: "tab-1"
            }
        },
        {
            command: "tab.close",
            payload: {
                tab: "tab-2"
            }
        }
    ]
});
{ "command": "batch", "payload": {"commands":[{"command":"tab.close","payload":{"tab":"tab-1"}},{"command":"tab.close","payload":{"tab":"tab-2"}}]} }