Sidecars

Create a shell Sidecar

Preview the real shell scaffold, define its interface, import it into Library and verify a target-specific package.

Alpha documentation

Create a small shell-owned panel, test it locally, then import it into Library and select it for an application. This guide uses the actual public scaffold rather than inventing a separate extension format.

Before you begin

You need an editor, a browser for the local surface check, and cApp with its owner service available for folder import. No bundler, wallet, model or backend credential is needed for the scaffold. For an application that only consumes an existing component, start with Build with Sidecars.

The public importer admits browser-based shell packages: HTML/CSS/JavaScript hosted by cApp's shell. That does not mean a standalone website automatically supports native Sidecars. It does not grant arbitrary host commands, executable paths or unreviewed native bridges. Platform declarations are author claims that you must test, not certifications issued by the importer.

Start from the public scaffold

Download the shell Sidecar scaffold. It contains the same four explicitly selected template files used by cApp's Library scaffold. Extract the archive and open the inner capp-user-sidecar-scaffold folder:

text
sidecar.json
runtime/my-sidecar-shell.js
runtime/my-sidecar-shell.css
README.md
FileResponsibility
sidecar.jsonIdentity, target declarations, package entries, defaults, Library form and Studio mapping
runtime/my-sidecar-shell.jsMounts the panel, reads selected configuration, handles its buttons and exposes its owner-window API
runtime/my-sidecar-shell.cssCanonical panel appearance and hidden state
README.mdPackage instructions to keep in sync with your own public interface

Use the online download when the Library's local scaffold-download service is unavailable. This public site does not connect to a fixed port on your machine. The scaffold starts hidden by default; it exposes an opener API but does not add a generic launcher button to every host. The local preview below supplies a button so manual mode is testable.

Give it an identity and a real scope

First run the unchanged scaffold locally. Then choose a stable reverse-domain ID you control before importing your own component. Keep that ID consistent in the manifest, the runtime's selected-configuration lookup and the ready-event detail. Also rename the root ID, CSS classes, data attributes, public global, visible text and Studio selectors when creating independently usable copies; changing only the manifest ID leaves collisions in the shell.

Give name, version, Library descriptions, permissions and capabilities real meanings. Bump the package version when changing a distributed interface or behaviour, not just its filename. A new ID is a different component, not an update of the old one.

The essential compile declaration is:

json
{
  "protocol": "capp.sidecar-package.v1",
  "id": "com.example.capp.sidecar.my-sidecar",
  "compile": {
    "mode": "shell",
    "nativeRuntime": false,
    "shellEntry": "runtime/my-sidecar-shell.js",
    "styleEntry": "runtime/my-sidecar-shell.css"
  }
}

This is a fragment, not a replacement for the complete scaffold manifest. Retain and review its platforms, runtimeReadyPlatforms, library and studio sections. runtimeReadyPlatforms must be a subset of platforms. The scaffold declares Windows, Linux and macOS; it does not declare Android or iOS. The importer recognises those mobile labels, but adding one is not proof of a mobile implementation, safe-area handling or touch usability.

compile.shellEntry is required. styleEntry and agentEntry are optional package-relative entries. Here, agent entry means an application-surface bridge script, not a registration of AI Agent tools or a permission grant. Keep entry paths within the package and distribute browser-executable UTF-8 assets; do not point the shell at an uncompiled TypeScript source or expect the importer to run npm scripts.

Configuration and the application interface

The runtime reads its matching package ID from window.__CAPP_EXECUTABLE_PLAN__.library.selected in the owning shell. Each selected entry may have config. Your code must consume the keys you declare; adding a Library field alone does not implement behaviour.

Scaffold settingDefaultRuntime behaviour
startupmanualMounts hidden; open makes the panel visible at startup
buttonLabelMy sidecarChanges the primary button's text

Keep manifest defaults, library.configSchema and runtime behaviour consistent. The scaffold's missing-config fallback is for this simple panel's own display settings. Do not generalise it into permission to start an alternative wallet, network or selected-but-unavailable provider. Configuration is read at mount; the scaffold has no live reconfiguration API.

The sample's owner-window interface is deliberately small:

Member or eventArguments / resultScope and effect
window.cappMySidecar.open()No arguments; returns undefinedShows the mounted panel and sets aria-hidden="false"
window.cappMySidecar.close()No arguments; returns undefinedHides it and sets aria-hidden="true"; does not remove the root
window.cappMySidecar.rootThe panel's DOM elementOwner-window inspection; not a serialisable cross-frame handle
capp:user-sidecar-readyCustomEvent.detail contains id and rootIdEmitted after the first mount/API publication, including a hidden mount

The primary action writes a local timestamp message into the panel. It does not save a record, call a service or return a business receipt. Re-evaluating the script with its existing root present avoids a second mount and does not emit another ready event. Listen before loading it, or inspect the already available API. Calling a method before the global exists is ordinary missing-interface behaviour, not a queued command.

These names do not register a portable SDK capability in every app iframe. A cross-surface consumer needs an explicit host-owned bridge with validated arguments and ownership. Do not pass DOM nodes over a message bridge, read arbitrary parent frames or add unrestricted postMessage handlers. Document your own result/error contract, subscriptions, cancellation and disposal when extending the sample. Close only hides this sample: it does not implement general provider disposal, focus trapping or keyboard-modal behaviour.

Preview the unchanged scaffold locally

Save this as preview.html beside sidecar.json in a temporary development copy and open it in a browser. It uses only the two local runtime files; no cApp service is contacted. Keep the preview out of your final distribution unless you deliberately intend to ship it.

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My Sidecar — local surface check</title>
  <link rel="stylesheet" href="runtime/my-sidecar-shell.css">
  <script>
    // A local display fixture, not a native capability or permission grant.
    window.__CAPP_EXECUTABLE_PLAN__ = {library: {selected: [{
      id: 'com.example.capp.sidecar.my-sidecar',
      config: {startup: 'manual', buttonLabel: 'Try the panel'}
    }]}};
    window.addEventListener('capp:user-sidecar-ready', event => {
      if (event.detail.id !== 'com.example.capp.sidecar.my-sidecar') return;
      document.getElementById('preview-open').disabled = false;
      document.getElementById('preview-status').textContent = 'Ready. Open the panel.';
    });
  </script>
  <script src="runtime/my-sidecar-shell.js" defer></script>
</head>
<body>
  <main>
    <h1>Local shell surface check</h1>
    <button id="preview-open" type="button" disabled>Open my Sidecar</button>
    <p id="preview-status" role="status">Waiting for the local runtime…</p>
    <p>The rest of your application remains usable after the panel closes.</p>
  </main>
  <script>
    document.getElementById('preview-open').addEventListener('click', () => {
      const api = window.cappMySidecar;
      if (api && typeof api.open === 'function') api.open();
      else document.getElementById('preview-status').textContent = 'Runtime unavailable.';
    });
  </script>
</body>
</html>

Expect Ready. Open the panel. before pressing the opener. The panel initially stays hidden. Open it, press Try the panel, check that the output changes, then close and reopen it. Change startup to open and reload to check launch-visible behaviour. After renaming the scaffold, update the fixture's package ID and global as well.

Test a label containing markup: it must display literally, not create elements or execute a handler. The scaffold uses textContent for the configurable label. Keep that boundary when customising it. This preview checks DOM behaviour only; it cannot validate native packaging, process privileges, an app bridge or a supported-device claim.

Import and test

In Library, expand Import or create a Sidecar. Choose Import globally or Import for this project, then select the folder containing sidecar.json, not the ZIP or its outer download folder. Project import requires an active project.

Import copies the package into the chosen managed scope; it does not keep a live link to your original development folder. Global availability is not global selection. A project-scoped package shadows a global package with the same ID for that project, regardless of which has the newer version. Built-in IDs cannot be replaced.

After import, check the reported name/version and invalid-package count. Add it to the intended app and configure it. For a first installed surface check, choose Open on launch: manual startup will otherwise remain hidden until an owner-window opener is supplied. Compile or select your app release, review the effective selection, package for a declared target and install. Check that the panel opens, changes its output and closes without breaking the app. Library and Build cover the surrounding workflow.

Package admission is not a security audit

The current host rejects a wrong protocol, reserved built-in ID, invalid entry path, missing required shell file, symlink, unsupported platform label, ready target outside the declared targets, and unreviewed native runtime/bridge declarations. The current admitted-file limit is 256, with a 12,000,000-byte total limit. Keep the distribution small and test it through the real importer.

Validation does not prove the JavaScript is safe or that capability/permission metadata is enforced as a sandbox around every browser operation. Review all scripts and assets. Remove secrets, hidden configuration, logs, development dependencies and unintended files before import or publication; ignored files are not a substitute for a deliberate distribution inventory. Do not request arbitrary host command execution through an unrelated bridge.

Update and troubleshoot

Edit your canonical source folder, bump the version, then re-import into the same scope. An existing ID requires explicit replacement confirmation. Replacement updates that scope's Package while preserving sibling Studio history. Refresh the Library and inspect the effective package/version before building again. A project override can still hide your new global copy.

Imported source and Studio edits are distinct. Existing Studio overrides may continue to affect a new package's labels/layout; review compatibility after changing selectors or functions. Neither replacing the package nor editing Studio rewrites an old immutable release or updates an installed binary automatically.

SymptomLikely next check
No sidecar.json foundExtract the ZIP and select the inner package folder
Import conflictConfirm the intended ID and scope before replacing; a new ID creates a second component
Import accepted but selection disabledimplementationStatus must be runtime-ready, Library must be selectable, and the app needs an identity
Runtime file missing or wrong targetEntry paths, compiled JavaScript and target declarations; do not bypass admission checks
Ready event fired but no panelstartup: manual is expected to be hidden; use the owner opener or select launch-visible mode
Wrong label/defaultsMatching package ID in the selected plan, configuration read at mount, then any Studio override
Global update seems ignoredA project-scoped package with the same ID takes precedence
Old appearance in the installed appConfirm new package selection/version, scoped Studio release and rebuilt binary
A second custom panel never mountsCheck root ID, global, CSS and selector collisions between renamed scaffolds

Make it reusable and maintainable

Keep canonical appearance in the package. Studio stores explicit global and project edits separately. Its declared surface selectors, regions and functions make intentional visual editing possible; the shell.close mapping and locked close action are not routes around native permission checks. Retain an accessible exit, keyboard access, readable labels and a mobile-safe layout for every target you actually support.

Document purpose, supported targets, setup, exact API names, method inputs/results, errors, persistence, cleanup and one repeatable example. State which UI owns the provider and whether multiple consumers can share it. Avoid adding a new broad bridge merely to expose a single action. Build with Sidecars explains discovery and application authority.

Ship a reviewed folder/ZIP with the manifest, referenced runtime assets and your README. Importing locally does not publish to the public directory, npm or Showroom. Showcase and publishing describes the separate public-listing workflow. Add brief versioned change notes explaining compatibility, required repackaging and any data migration; do not publish credentials, private records or engineering activity logs.

What success looks like

A fresh copy imports into the intended scope, appears with the correct identity, and materialises its shell assets for a declared target. The installed app uses its selected configuration, a manual opener works when supplied, closing preserves the app, and a second mount does not duplicate the panel. Your public API describes what was actually implemented—not additional native or Agent authority inferred from a manifest label.

↑ ↓ to navigate · Enter to open · Esc to close. Search stays in your browser.