Skip to content

feat: let surfaces describe their face, and plugins declare their models - #89

Open
Julusian wants to merge 4 commits into
mainfrom
claude/keen-tesla-ekqmu3
Open

Julusian wants to merge 4 commits into
mainfrom
claude/keen-tesla-ekqmu3

Conversation

@Julusian

@Julusian Julusian commented Sep 15, 2026

Copy link
Copy Markdown
Member

The intention here is to allow Companion to draw a fairly accurate representation of the surface, including overlaying any interactive controls.

This includes expecting modules to list their supported models and their visual representations, so that users can test previewing in different surface types

Summary by CodeRabbit

  • New Features
    • Plugins can declare supported surface models before a device connects.
    • Surface models now include stable identifiers, names, layouts, and optional visual appearances.
    • Surface appearances support dimensions, colors, artwork, and control shapes for richer surface rendering.
    • Host APIs expose surface model and appearance definitions, validation, and coverage helpers.
  • Bug Fixes
    • Invalid or incomplete appearances are safely discarded with a warning, while the surface remains available using layout-derived geometry.
  • Documentation
    • Added usage guidance for declaring models and describing surface appearances.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

📝 Walkthrough

Walkthrough

The change adds surface appearance schemas and validation, surface model declarations, generated schema coverage, plugin documentation, and host handling for model discovery and appearance data.

Changes

Surface capabilities

Layer / File(s) Summary
Appearance contract and validation
packages/base/src/surface-appearance-schema.ts, packages/base/src/surface-appearance.ts, packages/base/src/surface-api/types.ts, packages/base/src/schema-error.ts, packages/base/assets/*, packages/base/src/__tests__/*, packages/base/tools/*, packages/base/README.md
The base package defines appearance dimensions, colors, images, controls, shapes, labels, validation helpers, generated JSON Schema output, and control-coverage checks. Tests cover valid and invalid definitions. The README documents appearance usage.
Surface model API and validation
packages/base/src/surface-api/models.ts, packages/base/src/surface-api/plugin.ts, packages/base/src/surface-api/index.ts, packages/base/src/surface-model.ts, packages/base/src/main.ts, packages/base/src/__tests__/surface-model.spec.ts, packages/base/README.md
The base package adds surface model types, model ID constraints, the required getSurfaceModels() plugin method, model validation, public exports, tests, and model discovery documentation.
Host model and appearance integration
packages/host/src/plugin.ts, packages/host/src/types.ts, packages/host/src/main.ts, packages/host/src/__tests__/*
The host loads and validates models after plugin initialization, filters invalid or duplicate entries, stores cloned results, and exposes them through getSurfaceModels(). During surface opening, invalid or incomplete appearances are discarded and reported as null; valid appearances are returned in OpenDeviceResult.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to baa91

Existing plugins can fail to compile against the updated API, and a plugin-provided model can prevent initialization. These compatibility and availability issues should be resolved before merge.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes both primary changes: surface face descriptions and plugin-declared surface models.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/base/src/__tests__/surface-appearance.spec.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

packages/base/src/surface-appearance-schema.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

packages/host/src/__tests__/plugin.models.spec.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

  • 1 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Shapes bloom where surfaces meet
Models arrive on careful feet
Schemas guard each colored frame
Hosts keep opening all the same
Controls align in ordered rows

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: fda5bb2a-de03-40b8-8fa3-bf800b21d485

📥 Commits

Reviewing files that changed from the base of the PR and between 577cadb and e137934.

📒 Files selected for processing (23)
  • packages/base/README.md
  • packages/base/assets/surface-appearance.schema.json
  • packages/base/src/__tests__/schema-generation.spec.ts
  • packages/base/src/__tests__/surface-appearance.spec.ts
  • packages/base/src/__tests__/surface-model.spec.ts
  • packages/base/src/main.ts
  • packages/base/src/manifest.ts
  • packages/base/src/schema-error.ts
  • packages/base/src/surface-api/index.ts
  • packages/base/src/surface-api/models.ts
  • packages/base/src/surface-api/plugin.ts
  • packages/base/src/surface-api/types.ts
  • packages/base/src/surface-appearance-schema.ts
  • packages/base/src/surface-appearance.ts
  • packages/base/src/surface-layout.ts
  • packages/base/src/surface-model.ts
  • packages/base/tools/generate-schemas.mjs
  • packages/host/src/__tests__/plugin.appearance.spec.ts
  • packages/host/src/__tests__/plugin.models.spec.ts
  • packages/host/src/context.ts
  • packages/host/src/main.ts
  • packages/host/src/plugin.ts
  • packages/host/src/types.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread packages/base/README.md
// Clean up
}

async getSurfaceModels(): Promise<SurfaceModelDefinition[]> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Import SurfaceModelDefinition in this example.

The added method references a type that the example does not import. Copying this example causes Cannot find name 'SurfaceModelDefinition'.

Suggested correction
-import { SurfacePlugin, SurfaceContext } from '`@companion-surface/base`'
+import { SurfacePlugin, SurfaceContext, type SurfaceModelDefinition } from '`@companion-surface/base`'

*
* @param context Information about the host, for building the definitions against
*/
getSurfaceModels(context: SurfaceModelsContext): Promise<SurfaceModelDefinition[]>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep getSurfaceModels optional for existing plugins.

SurfacePlugin is an externally implemented interface. A required method breaks compilation for plugins that implement the previous contract.

Please declare getSurfaceModels? and treat an absent method as an empty model list in PluginWrapper. This preserves the stated compatibility with older modules and avoids logging their expected absence as a failure.

Based on learnings: introduce new plugin capabilities through an opt-in method so third-party implementations of the original contract remain compatible.

Proposed interface change
-	getSurfaceModels(context: SurfaceModelsContext): Promise<SurfaceModelDefinition[]>
+	getSurfaceModels?(context: SurfaceModelsContext): Promise<SurfaceModelDefinition[]>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getSurfaceModels(context: SurfaceModelsContext): Promise<SurfaceModelDefinition[]>
getSurfaceModels?(context: SurfaceModelsContext): Promise<SurfaceModelDefinition[]>

Source: Learnings

Comment thread packages/base/src/surface-appearance-schema.ts Outdated
Comment thread packages/host/src/plugin.ts Outdated
Comment thread packages/host/src/plugin.ts Outdated
this.#surfaceModels = valid

// Cloned so that a plugin cannot mutate what the host is holding, as with configFields
this.#host.surfaceModelsChanged?.(structuredClone(valid))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep model cloning inside the non-fatal boundary.

validateSurfaceModelDefinition validates the original object and discards the parsed schema result. An enumerable function-valued property therefore remains on a model with valid required fields. structuredClone(valid) then throws DataCloneError.

The clone runs outside the per-model try block, and PluginWrapper.init() awaits #loadSurfaceModels() without catching this error. Initialization can reject.

Clone or canonicalize each model inside its per-model try block, then add the cloned model to valid. Discard only models that cannot be cloned.

@Julusian
Julusian force-pushed the claude/keen-tesla-ekqmu3 branch 2 times, most recently from 6d591e0 to 56a07e3 Compare September 15, 2026 19:45
The same formatValidationError was copied into both manifest.ts and
surface-layout.ts. Move it to schema-error.ts so a third validator has
one to use rather than a third copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161mZMVRSUR82w2mTXfpnM7
@Julusian
Julusian force-pushed the claude/keen-tesla-ekqmu3 branch from 56a07e3 to ca83a90 Compare September 15, 2026 20:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 15a0df2b-6238-49d1-b090-a2ddf8c3b39c

📥 Commits

Reviewing files that changed from the base of the PR and between e137934 and ca83a90.

📒 Files selected for processing (16)
  • packages/base/README.md
  • packages/base/assets/surface-appearance.schema.json
  • packages/base/src/__tests__/surface-appearance.spec.ts
  • packages/base/src/__tests__/surface-model.spec.ts
  • packages/base/src/surface-api/models.ts
  • packages/base/src/surface-api/types.ts
  • packages/base/src/surface-appearance-schema.ts
  • packages/base/src/surface-appearance.ts
  • packages/base/src/surface-model.ts
  • packages/host/src/__tests__/plugin.appearance.spec.ts
  • packages/host/src/__tests__/plugin.models.spec.ts
  • packages/host/src/__tests__/surfaceProxy.leds.spec.ts
  • packages/host/src/__tests__/surfaceProxy.rotate.spec.ts
  • packages/host/src/main.ts
  • packages/host/src/plugin.ts
  • packages/host/src/types.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

* describing every one of them. Set to null if this surface has no face to describe, in which
* case it is drawn from geometry derived out of its layout instead.
*/
surfaceAppearance: SurfaceAppearanceDefinition | null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep surfaceAppearance optional for existing plugins.

This required member breaks existing SurfaceRegisterProps object literals at compile time. The host already treats an omitted value as null with surfaceAppearance ?? null.

Please make the property optional. This preserves source compatibility while new plugins can provide an appearance.

Proposed compatibility fix
-	surfaceAppearance: SurfaceAppearanceDefinition | null
+	surfaceAppearance?: SurfaceAppearanceDefinition | null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
surfaceAppearance: SurfaceAppearanceDefinition | null
surfaceAppearance?: SurfaceAppearanceDefinition | null

@Julusian
Julusian force-pushed the claude/keen-tesla-ekqmu3 branch from ca83a90 to c122238 Compare September 15, 2026 20:30
A layout says what controls a surface has and what to draw on them; it
says nothing about where they are, so anything drawing a picture of the
device has to guess. An appearance says it: a bounding box, artwork, and
a rectangle and shape per control, keyed by the same control ids.

bodyColor is required because it is the one thing that is useful with or
without artwork - it fills the face when there is none, and says what the
device looks like when there is, so a black device can be told apart from
a black page behind it.

Nothing consumes it yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161mZMVRSUR82w2mTXfpnM7
Required with an explicit null rather than optional, so that a module
author has to decide their surface has no face to describe instead of
never noticing they could describe one.

A malformed appearance, or one which misses a control the layout has, is
dropped with a warning and the surface still opens: how a device looks
may not stop it working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161mZMVRSUR82w2mTXfpnM7
@Julusian
Julusian force-pushed the claude/keen-tesla-ekqmu3 branch from c122238 to be6653c Compare September 16, 2026 14:21
A layout is only learnt when a surface connects, so a device cannot be
laid out before it is plugged in. getSurfaceModels asks the plugin what
it drives, once, after init. A method rather than a callback because a
method is the version a module author has to confront when they upgrade.

The wrapper holds what it gets, for its own caller to relay onwards. No
callback to the host: the list is fetched once and cannot change, so
anything named for a change would be advertising something that never
happens.

A model which does not validate is dropped on its own, and a plugin
which throws when asked still finishes initialising.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161mZMVRSUR82w2mTXfpnM7
@Julusian
Julusian force-pushed the claude/keen-tesla-ekqmu3 branch from be6653c to baa91b4 Compare September 16, 2026 14:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 0ad842ce-7099-423d-939b-1753bc321dc6

📥 Commits

Reviewing files that changed from the base of the PR and between ca83a90 and baa91b4.

📒 Files selected for processing (5)
  • packages/base/assets/surface-appearance.schema.json
  • packages/base/src/__tests__/surface-appearance.spec.ts
  • packages/base/src/surface-appearance-schema.ts
  • packages/host/src/__tests__/plugin.models.spec.ts
  • packages/host/src/plugin.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/host/src/plugin.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

export const MAX_BODY_IMAGE_LENGTH = 512 * 1024

/** Inline only - nothing downstream shares a filesystem with the module. */
const BODY_IMAGE_REGEX = /^data:image\/(svg\+xml|png|webp);base64,[A-Za-z0-9+/]+={0,2}$/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,125p' packages/base/src/surface-appearance-schema.ts
sed -n '1,120p' packages/base/assets/surface-appearance.schema.json
sed -n '150,205p' packages/base/src/__tests__/surface-appearance.spec.ts
sed -n '300,350p' packages/host/src/plugin.ts

Repository: bitfocus/companion-surface-api

Length of output: 14163


🏁 Script executed:

set -eu
rg -n -C 3 "validateSurfaceAppearance|BODY_IMAGE_REGEX|surface-appearance.schema.json|A=" packages/base packages/host

Repository: bitfocus/companion-surface-api

Length of output: 22415


Validate Base64 quantum grouping. An otherwise valid appearance containing data:image/png;base64,A= passes BODY_IMAGE_REGEX and validateSurfaceAppearance, so the host can retain it instead of applying the invalid-appearance fallback. The generated JSON schema has the same gap. Tighten the pattern, regenerate the schema, and add a regression test for this input.

-const BODY_IMAGE_REGEX = /^data:image\/(svg\+xml|png|webp);base64,[A-Za-z0-9+/]+={0,2}$/
+const BODY_IMAGE_REGEX =
+	/^data:image\/(svg\+xml|png|webp);base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)$/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants