Central views and sidebars
Section titled “Central views and sidebars”A central view opens as a tab and can be docked into a split layout. With multiple: false, opening it again selects the retained instance in the current workspace. With multiple: true, each opening creates a new instance.
A sidebar view is one instance per workspace and requires multiple: false. Its side and width are stored with the workspace layout. Both kinds require an open workspace.
If openView(id, initialState) reuses an existing instance, initialState does not replace its saved state. Use the view’s setState() callback for subsequent changes.
ViewProps
Section titled “ViewProps”| Prop | Meaning |
|---|---|
panel |
Saved descriptor of this view instance. |
active |
Whether this view is active. |
setState(state) |
Replaces the descriptor’s JSON state. |
onFocus() |
Reports focus to the workbench. |
onClose() |
Requests closure through the normal host flow. |
panel contains type: 'plugin', id, title, optional customTitle, owner, viewType, stateVersion and state.
Use ordinary buttons, inputs and links. The host captures focus inside the panel. Custom focus behavior can call onFocus(); data-plugin-focus can mark an element for pointer-focus support.
Persist display state
Section titled “Persist display state”Use setState() for a selected item, a filter or an expanded section. It replaces the entire JSON value, so include every field you intend to retain.
Supported values are null, booleans, strings, finite numbers, arrays and plain JSON objects. Functions, class instances, cyclic structures and reserved object keys (__proto__, constructor, prototype) are rejected.
| Limit | Value |
|---|---|
| Serialized UTF-8 size | 64 KiB |
| Visited values | 4096 |
| Nesting depth | 16 |
This state is part of the saved session. Do not use it as a store for unsaved file contents.
Example: filter and disclosure state
Section titled “Example: filter and disclosure state”Register this component instead of the template view. It reads older or empty state using safe defaults, preserving the other option when updating:
import type { Json, ViewProps } from '@lomi-dev/plugin-sdk';
type Options = { filter: string; expanded: boolean };
export function readOptions(value: Json): Options { const object = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; return { filter: typeof object.filter === 'string' ? object.filter : '', expanded: typeof object.expanded === 'boolean' ? object.expanded : true, };}
export function FilterPanel({ panel, setState }: ViewProps) { const options = readOptions(panel.state); return ( <section className="lomi-workspace-info"> <label> Filter <input value={options.filter} onChange={(event) => setState({ ...options, filter: event.target.value })} /> </label> <button type="button" aria-expanded={options.expanded} onClick={() => setState({ ...options, expanded: !options.expanded })} > Details </button> {options.expanded && <p>Current filter: {options.filter || 'All'}</p>} </section> );}setState() accepts a value, not a React-style setState(previous => ...) updater. Do not mutate panel.state directly in a running plugin. Each panel has its own ID and state; an active workspace change in useHostContext() does not replace that ID.
State compatibility
Section titled “State compatibility”A restored panel renders only when the declared stateVersion and placement match its descriptor. Changing the version does not invoke a migration function: SDK v1 has no state-migration hook.
For a compatible addition, retain the version and handle missing fields with defaults. For an incompatible change, expect existing panels to show an unavailable-view placeholder. Their old state remains until the user closes them or restores compatible code.
Protect unsaved documents
Section titled “Protect unsaved documents”Keep editable document data outside a transient React component, keyed by the retained panel ID. Register a guard once for that panel:
import type { DirtyView, PluginContext } from '@lomi-dev/plugin-sdk';
export function protectDocument( context: PluginContext, panelId: string, document: DirtyView,) { return context.registerDirtyView(panelId, document);}Your document object supplies:
title: a name shown in the close prompt, up to 160 characters.isDirty(): whether unsaved work remains.save(): a promise that resolves after saving succeeds.discard(): explicitly abandons the unsaved changes.
After save() resolves, isDirty() must return false. A rejection, thrown error or still-dirty result prevents closure. The panel must already exist and belong to this plugin; each panel can have only one guard.
Retain the guard while the panel exists, including while its component is temporarily unmounted. Do not tie the guard’s lifetime to a React effect cleanup that runs during layout changes. The host drops guards for closed panels and releases registrations on deactivation; your document store and other resources still need their own cleanup.
Disabling or uninstalling a plugin uses these guards before removing its contributions. Canceling a close leaves the plugin running.
Example unsaved-document model
Section titled “Example unsaved-document model”This function wraps an existing model in a close guard. persist is your persistence implementation, not an SDK method: the public SDK has no filesystem service.
import type { DirtyView } from '@lomi-dev/plugin-sdk';
type DocumentModel = { title: string; text: string; savedText: string };
export function documentGuard( model: DocumentModel, persist: (text: string) => Promise<void>,): DirtyView { return { title: model.title, isDirty: () => model.text !== model.savedText, async save() { const submittedText = model.text; await persist(submittedText); model.savedText = submittedText; }, discard() { model.text = model.savedText; }, };}After openView() returns a panel ID, create or retrieve the model in your plugin store and call registerDirtyView(panelId, documentGuard(model, persist)) once. If text changes during saving, isDirty() remains true, preventing closure with the new unsaved edit.
A guard does not save automatically or restore unsaved text after a process crash. The SDK also provides no complete panel list or per-panel closed event. Design bounded document storage and cleanup; host removal of a guard does not remove your model.
A component’s onClose() only requests closure. It does not confirm user approval, so do not immediately delete unsaved data when calling it. Verify closing behavior in the installed application.
Source: view rendering and compatibility, state validation, panel creation.