@molecule/app-ide-default
Provider bond · ide · App (browser) · v1.0.2 · Apache-2.0
Default workspace layout provider with panel management
npm install @molecule/app-ide-defaultnpm · Source on GitHub · Implements @molecule/app-ide
How it works
@molecule/app-ide-default is a provider bond on the app (browser) side: it implements the ide core interface (@molecule/app-ide) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setProvider } from '@molecule/app-ide'
import { provider } from '@molecule/app-ide-default'
setProvider(provider) // once, at app startup (bonds.ts)Reference
Default IDE workspace provider for molecule.dev — panel layout state (chat / editor / preview), sizes, collapse/active state, with optional persistence through an injectable storage adapter.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setProvider } from '@molecule/app-ide'
import { provider } from '@molecule/app-ide-default'
setProvider(provider) // once, at app startup (bonds.ts)
Type
provider
Installation
npm install @molecule/app-ide-default @molecule/app-ide
API
Interfaces
StorageAdapter
Adapter interface for storage.
interface StorageAdapter {
getItem(key: string): string | null
setItem(key: string, value: string): void
removeItem(key: string): void
}
DefaultWorkspaceConfig
Configuration for default workspace.
interface DefaultWorkspaceConfig {
/** Default panel layout. */
defaultLayout?: WorkspaceLayout
/** Persist layout using the provided storage adapter. */
persistLayout?: boolean
/** Key for storage persistence. */
storageKey?: string
/** Storage adapter for persistence. Required when persistLayout is true. */
storage?: StorageAdapter
}
Classes
DefaultWorkspaceProvider
Default implementation of WorkspaceProvider. Manages panel layout (chat, editor, preview),
sizes, visibility, collapse state, and optional persistence via a storage adapter.
class DefaultWorkspaceProvider implements WorkspaceProvider {
readonly name = 'default'
private state: WorkspaceState
private defaultLayout: WorkspaceLayout
private persistLayout: boolean
private storageKey: string
private storage: StorageAdapter | null
private subscribers: Set<(state: WorkspaceState) => void> = new Set()
constructor(config: DefaultWorkspaceConfig = {}) {
this.defaultLayout = config.defaultLayout ?? DEFAULT_LAYOUT
this.persistLayout = config.persistLayout ?? false
this.storageKey = config.storageKey ?? 'molecule-workspace-layout'
this.storage = config.storage ?? null
const savedLayout = this.loadPersistedLayout()
this.state = {
layout: savedLayout ?? { ...this.defaultLayout },
activePanel: null,
collapsedPanels: new Set(),
isFullscreen: false,
}
}
/**
* Returns the current workspace layout with panel positions and sizes.
* @returns The current workspace layout.
*/
getLayout(): WorkspaceLayout {
return this.state.layout
}
/**
* Replaces the workspace layout, persists it, and notifies subscribers.
* @param layout - The new workspace layout.
*/
setLayout(layout: WorkspaceLayout): void {
this.state = { ...this.state, layout }
this.persist()
this.notify()
}
/**
* Toggles a panel's collapsed state.
* @param panelId - The ID of the panel to toggle.
*/
togglePanel(panelId: PanelId): void {
const collapsed = new Set(this.state.collapsedPanels)
if (collapsed.has(panelId)) {
collapsed.delete(panelId)
} else {
collapsed.add(panelId)
}
this.state = { ...this.state, collapsedPanels: collapsed }
this.notify()
}
/**
* Resizes a panel to the given size percentage within its position group.
* @param panelId - The ID of the panel to resize.
* @param size - The new size as a percentage.
*/
resizePanel(panelId: PanelId, size: number): void {
const panel = this.state.layout.panels.find((p) => p.id === panelId)
if (!panel) return
const position = panel.position
const positionPanels = this.state.layout.panels.filter((p) => p.position === position)
const panelIndex = positionPanels.findIndex((p) => p.id === panelId)
if (panelIndex === -1) return
const sizes = { ...this.state.layout.sizes }
const positionSizes = [...(sizes[position] ?? [])]
positionSizes[panelIndex] = size
sizes[position] = positionSizes
this.state = {
...this.state,
layout: { ...this.state.layout, sizes },
}
this.persist()
this.notify()
}
/**
* Sets the given panel as the active (focused) panel.
* @param panelId - The ID of the panel to activate.
*/
setActivePanel(panelId: PanelId): void {
this.state = { ...this.state, activePanel: panelId }
this.notify()
}
/**
* Subscribes to workspace state changes.
* @param callback - Called with the new state whenever layout, panels, or active panel changes.
* @returns An unsubscribe function.
*/
subscribe(callback: (state: WorkspaceState) => void): () => void {
this.subscribers.add(callback)
return () => this.subscribers.delete(callback)
}
/** Resets the workspace to the default layout, clears collapsed panels, and persists. */
resetLayout(): void {
this.state = {
layout: { ...this.defaultLayout },
activePanel: null,
collapsedPanels: new Set(),
isFullscreen: false,
}
this.persist()
this.notify()
}
/** Notifies all subscribers of the current workspace state. */
private notify(): void {
for (const cb of this.subscribers) {
cb(this.state)
}
}
/** Persists the current layout to the storage adapter if persistence is enabled. */
private persist(): void {
if (!this.persistLayout || !this.storage) return
try {
const serializable = {
layout: this.state.layout,
collapsedPanels: [...this.state.collapsedPanels],
}
this.storage.setItem(this.storageKey, JSON.stringify(serializable))
} catch (_error) {
// storage may be unavailable (quota exceeded, security restrictions) — persistence is best-effort
}
}
/**
* Loads a previously persisted layout from the storage adapter.
* @returns The saved layout, or `null` if none exists or persistence is disabled.
*/
private loadPersistedLayout(): WorkspaceLayout | null {
if (!this.persistLayout || !this.storage) return null
try {
const saved = this.storage.getItem(this.storageKey)
if (!saved) return null
const parsed = JSON.parse(saved) as { layout?: WorkspaceLayout }
return parsed.layout ?? null
} catch (_error) {
// corrupted or unreadable saved data — fall back to the default layout
return null
}
}
}
Functions
getBrowserStorage()
Returns a StorageAdapter backed by the browser's localStorage, or
null when no usable localStorage exists. A null result means
"persistence unavailable — fall back to in-memory".
function getBrowserStorage(): StorageAdapter | null
Returns: A localStorage-backed adapter, or null if unavailable.
createProvider(config)
Creates a DefaultWorkspaceProvider with optional layout configuration and persistence.
function createProvider(config?: DefaultWorkspaceConfig): DefaultWorkspaceProvider
config— Workspace configuration (default layout, persistence options, storage adapter).
Returns: A DefaultWorkspaceProvider that manages panel layout state.
Constants
provider
Pre-instantiated provider singleton. Persists the layout to the browser's
localStorage when available, so a panel resize survives a page reload;
falls back to a non-persistent in-memory provider where storage is missing
or blocked (SSR, tests). Consumers wanting different behavior (a custom
storage adapter, key, or opting out) should call createProvider.
const provider: DefaultWorkspaceProvider
Core Interface
Implements @molecule/app-ide interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/app-ide'
import { provider } from '@molecule/app-ide-default'
export function setupIdeDefault(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/app-ide^1.0.1
Runtime Dependencies
-
@molecule/app-ide -
A persisted layout SHADOWS default-layout changes. The
providersingleton saves tolocalStoragekey'molecule-workspace-layout'; once a user has a saved layout, changes todefaultLayout(or the built-in default) have no visible effect untilresetLayout()runs or the key is cleared. If "my layout change doesn't show up", this is why. -
Default layout:
chat(left, 25%) /editor(center, 50%) /preview(right, 25%).PanelIdaccepts custom strings — extend the layout rather than building a parallel one. -
State-only: pair with
@molecule/app-ide-react(or your own renderer readinggetLayout()+subscribe()). -
Need different persistence (custom adapter, key, or none)? Build your own instance with
createProvider({...})instead of the singleton.
E2E Tests
Integration checklist — drive the real rendered UI (live preview, no mocks):
navigate_preview to the IDE, read_preview_ui to snapshot the panel
regions, interact_preview to drag dividers and toggle panels. Adapt each
item to this app's actual panels/layout and check every box off one by one.
A box you can't check is an integration bug to fix — not a skip:
- The IDE renders its panel regions in the default layout — snapshot the preview and confirm each visible panel the app configures (e.g. a left sidebar/files, a center editor, a right preview, a bottom terminal) is present and laid out, none overlapping or collapsed to nothing.
- Dragging a divider between two panels resizes the adjacent panels and the sizes update live — grab the handle, drag, and confirm in a fresh snapshot that both neighbors changed size (not the whole window, no snap-back to the previous sizes).
- Toggling a panel's visibility (hide the terminal or the sidebar via its control) removes it from the layout, and toggling again restores it in the same position — the neighbors reflow to fill, they don't leave a blank gap.
- Collapsing a collapsible panel shrinks it out of the way and expanding restores its prior size; clicking into a panel updates the active-panel state (its highlight/toolbar follows the panel you focus).
- A resized / collapsed / hidden layout PERSISTS across a full reload — after reload the panels return at the sizes and visibility you left them, not reset to the default layout (the default bond persists to browser storage).
- A panel's minimum size is respected — dragging a divider to the far end cannot shrink a resizable panel to zero or an unusable sliver; it stops at its configured min.