Files
quixos/docs/browser-surfaces.md
Timothy J. Aveni cf83241e8c Rename fx5 -> quixos
I did some of this by hand (thru control-cli / docs), then let codex take over
2026-05-24 16:07:11 -07:00

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.stateContext surface 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:

  • stateContextId
  • surfaceId
  • hostSessionId

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 } from propsUpdate(...) 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 uiActions stream

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:

  • surfaceId
  • runtime: "browser-module"
  • transport: "relay"
  • propsFunction
  • surface intent hints like surfaces, capabilities, and aspectRatioHint

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 component
  • surface-entry.tsx: browser mount shim
  • surfaceTypes.ts: props/action/output schemas and inferred types
  • server.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-surface
    • createEmbeddedSurfaceRef(...)
    • createEmbeddedSurfaceRefSchema(...)
  • @quixos/package-runtime/browser-surface/react
    • EmbeddedSurface
    • createEmbeddedSurfaceComponent(...)

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.