I did some of this by hand (thru control-cli / docs), then let codex take over
8.9 KiB
Browser Surfaces
This document covers the relay-backed browser UI model used by react-surface packages.
Model
Browser surfaces do not push UI through normal Quixos function/event/value transport.
Instead:
- the package server owns the authoritative per-
ctx.stateContextsurface instance - it registers one or more browser bundles and websocket sessions with the shared surface relay
- browser or server-side hosts mount that bundle with
mount(container, api) - props flow down from the package server through the relay
- typed browser actions flow back up through the relay to the package server
- semantic outputs still come out through normal Quixos events, values, and functions
The live browser session is keyed by:
stateContextIdsurfaceIdhostSessionId
Browser-side surface failures should be treated as package failures when they are tied to a specific mounted surface. Render errors, props-parsing failures, and similar client-side surface faults should be reported back through the relay so they appear in qx-control errors for the owning package instead of only disappearing into a browser console.
Package Shape
For a browser-mounted surface package:
- use
propsUpdate({ props })as the surface input function - return
{ stateContextId }frompropsUpdate(...)so hosts can attach to the right surface instance - keep browser action unions internal to the surface transport
- translate browser actions into explicit semantic Quixos events or values for dependents instead of exposing one generic
uiActionsstream
Treat stateContextId as internal plumbing. Do not show raw state-context strings to normal users except in explicit debugging or admin views.
Local React state should be kept to truly transient UI concerns. Avoid storing business state or durable entity state in the browser component when it should live in the package server or a parent package. Lift state out of the client whenever it needs to be shared, replayed, observed, or controlled by dependents.
Annotations
Exported UI surfaces should use repeated quixos.ui.surface/v1 annotations.
For browser-mounted surfaces, include fields such as:
surfaceIdruntime: "browser-module"transport: "relay"propsFunction- surface intent hints like
surfaces,capabilities, andaspectRatioHint
Example:
{
type: "quixos.ui.surface/v1",
surfaceId: "desktop",
runtime: "browser-module",
transport: "relay",
propsFunction: "propsUpdate",
surfaces: ["desktop"],
capabilities: ["mouse", "keyboard"],
aspectRatioHint: "16:10",
}
Template Boundaries
The react-surface template is intentionally split so package authors mostly work in application code:
SurfaceView.tsx: application-facing React componentsurface-entry.tsx: browser mount shimsurfaceTypes.ts: props/action/output schemas and inferred typesserver.ts: package-specific state transitions and semantic output emission
Keep the shared transport layer in @quixos/package-runtime, not copied into each package. In practice:
@quixos/package-runtime/browser-surface- relay/session controller
- event hub
- surface schema helpers
propsUpdate/{ stateContextId }boilerplate
@quixos/package-runtime/browser-surface/react- React mount shim
- props/action parsing glue
- client-side error-boundary reporting
The template and real surface packages should stay thin. If the same browser-surface helper logic shows up in more than one package, move it into the runtime instead of copying it forward again.
Presentation
The default posture for generated Quixos surfaces should be compact and utilitarian.
- avoid oversized padding and oversized chrome
- prefer dense, legible layouts that behave like productivity tools
- do not surface internal ids or transport plumbing in the normal UI
- only lean into conspicuous styling when the user is actually asking for something aesthetic or expressive
- assume a browser surface may be used as the entire screen or dropped into a parent container
- do not add gratuitous card chrome such as outer shadows, borders, rounded-corner shells, or fake app frames around the whole surface unless the user explicitly wants that look
- avoid user-facing headers, introductory instructions, and descriptive title text inside the surface unless the user specifically asks for them
- write UI copy for the actual end user of the surface, not for someone who knows the Quixos package layout
- do not describe a surface in package-structure terms like "mounted from root" or "orchestration package" unless the view is explicitly a developer/admin surface
Headers and explanatory text consume scarce space, make surfaces awkward to compose inside parent surfaces, and are usually redundant because hosts such as the project visualizer already provide surface labels.
Pop-out or shared-link use is common for browser surfaces. Surfaces should hold up when opened on their own in a new tab, popup window, or phone-sized view from a QR/shared link, not just when embedded in the main visualizer.
Composition
Browser surfaces can embed other browser surfaces, but the package server must own that composition.
For more composition-specific guidance and examples, see docs/browser-surface-composition.md.
- do not call
usePackage(...)from browser code - do not let the browser open child state contexts
- create child surface state contexts on the server, then pass those child refs down as props
Use the shared helpers:
@quixos/package-runtime/browser-surfacecreateEmbeddedSurfaceRef(...)createEmbeddedSurfaceRefSchema(...)
@quixos/package-runtime/browser-surface/reactEmbeddedSurfacecreateEmbeddedSurfaceComponent(...)
Typical server-side flow:
const childSurface = ctx.usePackage(childUiSchema);
const { stateContextId } = await childSurface.functions.propsUpdate({
props: { title: "Child" },
});
instance.state.props.child = createEmbeddedSurfaceRef(childUiSchema, {
stateContextId,
});
Typical browser-side flow:
const ChildSurface = createEmbeddedSurfaceComponent(childUiSchema);
<ChildSurface stateContextId={props.child.stateContextId} />
Or use the lower-level ref directly:
<EmbeddedSurface surface={props.child} />
The parent decides layout, sizing, and visibility. The child surface keeps its own relay-backed props/actions lifecycle.
Lifecycle
Create dependency clients with ctx.usePackage(...) inside onContextOpen(ctx) or another handler with ctx. Do not create dependency clients at module scope or use onCreate for context-scoped work.
Use onContextClose(ctx) for per-state-context cleanup. Tear down:
- relay registrations
- per-context render state
- subscriptions
- workers
- other context-scoped resources
For important server-side surface state, persist it under ctx.stateDirectory. Read it in onContextOpen, write it back after meaningful mutations, and treat onContextClose as a final best-effort flush rather than the only persistence point.
Root And Program Structure
Treat exported React components as UI surfaces. The default visualizer pane unwraps state contexts under root, renders exported surfaces inline, and keeps non-surface nodes collapsed but visible as structure.
root should not itself be a surface package. Treat it as the orchestration layer that instantiates UI sibling packages. If you need a new UI, make a new package for that surface and wire it under root; do not add another direct React surface to root.
In general, if a package has meaningful interactive, visual, or operational state, it should have a corresponding browser surface package. In practice, new business-logic packages should be assumed to need sibling UI packages unless there is a very explicit reason not to.
Generated browser surfaces are especially useful for:
- collecting input
- presenting output
- showing debug or status information
- collecting and editing configuration
Do not wait for an explicit request before exposing useful debug/status UI. Treat it like an info-level logging surface: if it helps operate or understand the system, it is usually worth providing.
When planning a task, start by deciding how the microservice state should be made visible and operable through sibling UI packages. When one component has distinct operator jobs, prefer splitting surfaces reasonably by purpose. Common examples:
- one surface for operating or driving the component
- one surface for configuration
- one compact status or debug surface
This keeps each surface concise and easier to wire into root.
If a surface belongs in the program structure, prefer declaring it as a dependency of root and instantiating it from the root tree. In practice that usually means:
ctx.usePackage(...)- then whatever surface-specific setup you need, such as
propsUpdate(...)
ctx.usePackage(...) boots the dependency package and opens its derived child state context. Additional calls such as propsUpdate(...) feed surface data into that child once it exists.