@pwfabric/phico — composer kernel
@pwfabric/phico is the surface authoring kernel that lives behind the
PhiCo Studio. It bundles the Zustand-based editor store, the block and
capability registries, the composer mutation executor and stream
client, plus the React hooks that wire drag-and-drop, clipboard,
keyboard shortcuts, validation, and capability detection into a
consuming app shell. AI runtime adapters (assistant-ui chat model,
feedback adapter, history compaction) ship under the ./ai subpath.
Every export listed here is intentional — the package barrel is an explicit named-export list (see ADR-152). Internal helpers stay file-scoped and are not part of the public API.
Installation
pnpm add @pwfabric/phico@pwfabric/phico requires React 19 and Node ^22. It peer-depends on
@pwfabric/contracts, @pwfabric/core, and @pwfabric/runtime.
Editor store
The store is a Zustand store holding the surface document, current selection, viewport, composer phase, and the explorer / right-panel UI state. Mutate it through the action methods on the store; never reach into the state shape directly.
import { useEditorStore, type SurfaceDefinition } from '@pwfabric/phico'
function loadSurface(surface: SurfaceDefinition): void {
useEditorStore.getState().setSurface(surface)
}
function MyComponent() {
const surface = useEditorStore((s) => s.surface)
const isDirty = useEditorStore((s) => s.isDirty)
return <div>{surface?.name} {isDirty && '*'}</div>
}Selectors
These hooks return derived state with shallow comparison so consumers re-render only when the relevant slice changes.
| Hook | Returns |
|---|---|
useSelectedBlock() | The single selected Block or null. |
useSelectedBlocks() | Array of selected blocks (multi-select aware). |
useIsBlockSelected(blockId) | true when the block id is in the current selection. |
useSelectionCount() | Number of selected blocks. |
useIsCanvasLocked() | true when the canvas is in a non-editable phase (composing / review). |
useIsComposing() | true while an AI composer turn is in flight. |
useBlockStyleValue(blockId, path) | Resolves a style value with source attribution (override, token-bound, theme, default). |
Mutation sink
setMutationSink() registers a callback that fires every time the
store applies a SurfaceMutation. Pair it with
useClientMutationBuffer (or your own buffer) to ship mutations to the
backend.
import { setMutationSink } from '@pwfabric/phico'
setMutationSink((mutation) => {
buffer.enqueue(mutation)
})Block registry
The block registry maps block types (heading, text, container, …)
to their renderer, property schema, and category metadata. Both a
class (for explicit setup) and a singleton getter are exposed.
import { getBlockRegistry, useBlockRegistry } from '@pwfabric/phico'
// Imperative (outside React):
const registry = getBlockRegistry()
const headingEntry = registry.get('heading')
// React hook:
function BlockTypePicker() {
const registry = useBlockRegistry()
return <ul>{registry.types().map(t => <li key={t}>{t}</li>)}</ul>
}| Export | Shape |
|---|---|
getBlockRegistry() | Returns the shared BlockRegistry singleton. |
useBlockRegistry() | React hook returning the same singleton, stable across renders. |
BlockRegistry (class) | Construct your own registry; useful in tests. |
StudioBlockEntry | Registry entry shape — renderer + schema + category metadata. |
PropertySchema, PropertyType | Property metadata describing how the inspector renders a field. |
BlockCategory, CategoryDefinition | Block grouping in the palette. |
Composer executor
executeSingleMutation and executePlan apply composer-emitted
mutations to the live editor store. They are the receipt-lifecycle
sink for the streaming composer.
import {
executeSingleMutation,
executePlan,
resetStreamingState,
streamingSeenIds,
type ComposerMutation,
type ComposerPlan,
type ExecutionResult,
} from '@pwfabric/phico'
// Apply a single streamed mutation (idempotent via streamingSeenIds):
function onMutation(m: ComposerMutation): void {
const store = useEditorStore.getState()
executeSingleMutation(m, store, streamingSeenIds)
}
// Apply a full plan (e.g. composer returned the final receipt):
function onPlan(plan: ComposerPlan): ExecutionResult {
return executePlan(plan)
}
// Reset between composer sessions:
resetStreamingState()| Export | Purpose |
|---|---|
executeSingleMutation(mutation, store, seenIds) | Apply one mutation, deduplicating via seenIds. |
executePlan(plan) | Apply every mutation in plan.mutations. Returns ExecutionResult. |
resetStreamingState() | Clear the dedupe set between composer turns. |
streamingSeenIds | The shared Set<string> used for dedupe (exposed so consumers and tests can inspect it). |
BlockSnapshot, DataSourceSnapshot | Composer wire types — un-normalised inputs the executor sanitises. |
ComposerMutation, ComposerPlan | The receipt-shaped wire format the composer emits. |
ExecutionResult | Outcome of applying a plan (touched ids, errors). |
Composer stream client
streamComposerRequest opens an SSE stream to the composer endpoint
and dispatches structured events through StreamCallbacks. The
returned promise resolves once the stream is fully drained.
import {
streamComposerRequest,
type StreamCallbacks,
type ComposerStreamRequest,
} from '@pwfabric/phico'
const callbacks: StreamCallbacks = {
onMutation: (m) => executeSingleMutation(m, store, streamingSeenIds),
onPlan: (p) => onPlan(p),
onSuggestions: (s) => setSuggestions(s),
onValidationRejection: (r) => showRejection(r),
onCreditMeta: (meta) => updateCreditBadge(meta),
}
const request: ComposerStreamRequest = { intent, context, history }
await streamComposerRequest({ baseUrl, headers, request, callbacks })CompletionMeta, ComposerSuggestion, and ValidationRejection are
the discriminated payload shapes carried by the stream.
Block tree utilities
Pure helpers for traversing the block tree — useful when consumers need to compute things outside the store or write tests.
import {
findBlockById,
findBlockParent,
getBlockDepth,
getMaxSubtreeDepth,
} from '@pwfabric/phico'
const block = findBlockById(blocks, 'block-42')
const parent = findBlockParent(blocks, 'block-42')
const depth = getBlockDepth(blocks, 'block-42') // 0 for root
const maxDepth = getMaxSubtreeDepth(block!) // longest descendant chainSurface paths (ADR-124)
Helpers that read and ensure the consolidated surface shape — theme,
style variables, persistence config, and data sources live under
surface.appearance.config.* and inside the platform/persistence
capability config respectively.
import {
ensureAppearanceConfig,
getTheme,
getStyleVariables,
ensurePersistenceConfig,
getDataSources,
getInitialState,
emptyAppearance,
DEFAULT_APPEARANCE_ATOM_ID,
} from '@pwfabric/phico'
// Read:
const theme = getTheme(surface)
const variables = getStyleVariables(surface)
const dataSources = getDataSources(surface)
// Mutate (within a draft):
const cfg = ensureAppearanceConfig(draft)
cfg.theme.colors.primary = '#6366f1'
const persistence = ensurePersistenceConfig(draft)
persistence.dataSources = [...]Layout utilities (ADR-025)
import {
resolveSpan,
getEffectiveColumns,
getDefaultBlockLayout,
getBreakpointDefinition,
getParentColumns,
} from '@pwfabric/phico'
// How many columns does this block span at the current breakpoint?
const span = resolveSpan(block, breakpoint)
// How many columns does its parent contribute?
const cols = getParentColumns(blocks, block.id)Drag and drop
import {
useDragStore,
isValidDropTarget,
calculateDropPosition,
getDropIndex,
setDragData,
getDragData,
hasDragData,
} from '@pwfabric/phico'
function handleDragOver(event: DragEvent, target: DropTarget) {
if (isValidDropTarget(useDragStore.getState().dragItem, target)) {
event.preventDefault()
}
}DragItem / DropTarget / DragState / DragActions / DragStore
are the corresponding types.
Clipboard
import {
useClipboardStore,
useHasClipboard,
useClipboardCount,
} from '@pwfabric/phico'
function ToolbarButton() {
const hasClipboard = useHasClipboard()
const count = useClipboardCount()
return <button disabled={!hasClipboard}>Paste ({count})</button>
}Keyboard
import {
KeyboardManager,
getKeyboardManager,
defaultShortcuts,
} from '@pwfabric/phico'
useEffect(() => {
const manager = getKeyboardManager()
return manager.bind(defaultShortcuts, handlers)
}, [handlers])Validation
Block-level + surface-level validators producing structured
ValidationIssues the panel renders.
import {
validateBlock,
validateSurface,
getBlockIssues,
getSeverityIcon,
} from '@pwfabric/phico'
const issues = validateSurface(surface).issues
const blockIssues = getBlockIssues(issues, block.id)Capability hooks
Detect which capabilities a surface implicitly needs (based on its blocks) and validate the user-supplied config against each capability’s schema.
import {
useCapabilityDetection,
detectCapabilitiesFromBlocks,
useCapabilityValidation,
getCapabilityConfigSchema,
getCapabilityDisplayInfo,
} from '@pwfabric/phico'
function CapabilityPanel() {
const suggestions = useCapabilityDetection()
const validation = useCapabilityValidation()
return (
<ul>
{suggestions.map((s) => (
<li key={s.capability}>
{getCapabilityDisplayInfo(s.capability).label}
</li>
))}
</ul>
)
}Command history (undo / redo)
import {
useCommandHistory,
createAddBlockCommand,
createRemoveBlockCommand,
createUpdateBlockCommand,
createMoveBlockCommand,
createUpdateNameCommand,
} from '@pwfabric/phico'
const history = useCommandHistory()
history.push(createAddBlockCommand({ block, parentId, index }))
history.undo()
history.redo()Editor commands
Convenience commands for common toolbar actions and a useEditorCommands
hook that wires them to keyboard shortcuts.
import {
deleteSelectedBlock,
duplicateSelectedBlocks,
selectAllBlocks,
clearSelection,
zoomIn,
zoomOut,
zoomReset,
toggleExplorer,
useEditorCommands,
createSaveHandler,
} from '@pwfabric/phico'
useEditorCommands({
onSave: createSaveHandler(saveSurface),
})Theme presets (ADR-041)
Ready-to-use surface theme presets and the corresponding type.
import { SURFACE_THEME_PRESETS, type SurfaceThemePreset } from '@pwfabric/phico'
const preset: SurfaceThemePreset = SURFACE_THEME_PRESETS[0]
applyPreset(preset)AI subpath (@pwfabric/phico/ai)
The ./ai subpath ships the assistant-ui chat-model adapter plus the
streaming and history utilities the Studio uses.
import {
createComposerAdapter,
type ComposerApiClient,
type ComposerRuntimeCallbacks,
createFeedbackAdapter,
createLocalStorageHistoryAdapter,
clearLocalStorageHistory,
compactHistory,
streamToAssistantParts,
fetchWithRetry,
useKeyboardAvoidance,
pushComposerAcceptCommand,
} from '@pwfabric/phico/ai'
const adapter = createComposerAdapter(
runtimeCallbacks,
() => selectedModelId,
() => availableBlockTypes,
() => getApiClient(), // inject your app-shell api client
)createComposerAdapter accepts a getApiClient getter typed as the
minimal ComposerApiClient interface ({ url, getHeaders() }). This
keeps the package independent of any specific app-shell library.
useKeyboardAvoidance reports the on-screen keyboard offset on mobile
so the composer input can re-position itself. pushComposerAcceptCommand
records an accept event into the command history so users can undo it.
TypeScript types
Every export above ships with TypeScript types. Common surface types are re-exported through the editor-store barrel:
import type {
Block,
SurfaceDefinition,
SurfaceTheme,
SurfaceThemeColors,
SurfaceThemeBorder,
SurfaceThemeTypography,
SurfaceThemeSpacing,
SurfaceThemeShadows,
SurfaceThemeMotion,
SurfaceThemeLayout,
LayoutConfig,
ExplorerTab,
StudioMode,
ComposerPhase,
CapabilityRequirement,
Selection,
SelectionModifier,
EditorState,
EditorActions,
EditorStore,
StyleSource,
SurfaceDataSource,
} from '@pwfabric/phico'See also
defineBlock()— declare a custom block type the registry can render.defineCapability()— declare a capability the composer can wire into a surface.defineFactory()— bundle a set of blocks and capabilities as a single deployable atom.- Surface OS layers — where the composer kernel fits in the 14-layer model (ADR-153).