Skip to content

Events and environment API ​

Three related pieces answer "what can my app do right now?": client.environment for a snapshot, client.services for the method-by-method picture, and client.events for a stream that tells you when either changes.

Availability ​

None of these need a permission. They describe what you already have, so they work in every workspace, connected or not.

Interface ​

ts
interface AppEnvironment {
  readonly apiVersion: 1;
  readonly client?: { readonly platform: "windows" | "macos" | "linux" | "android" | "ios" | "web" | "unknown" };
  readonly connection: "local" | "connected" | "disconnected" | "review-required";
  readonly binding: string | null;          // opaque identity of the current connection
  readonly workspaceId?: string | null;     // opaque identity of the configured target; "local" without a session
  readonly host?: { readonly name: string; readonly system: string; readonly target?: string };
  readonly visible: boolean;
  readonly capabilities: readonly string[];
  readonly operations?: Readonly<Record<string, readonly string[]>>;
}

interface ServiceMethodInfo {
  readonly name: string;                    // for example "system.files.readText"
  readonly version: number;
  readonly source?: string;                 // present for custom services
  readonly permissions: readonly string[];
  readonly granted: boolean;                // your app holds every permission it needs
  readonly available: boolean;              // the service can serve it right now
}

interface AppEvent      { readonly sequence: number; readonly topic: string; readonly value: Json }
interface AppEventBatch { readonly cursor: number; readonly reset: boolean; readonly events: readonly AppEvent[] }

client.environment.get(signal?: AbortSignal): Promise<AppEnvironment>;
client.services.list(signal?: AbortSignal): Promise<readonly ServiceMethodInfo[]>;
client.events.subscribe(listener: (batch: AppEventBatch) => void | Promise<void>, onError?: (error: unknown) => void): () => void;

connection is the state that matters most:

ValueMeaning
localThe local desktop workspace. No host at all.
connectedA host is connected and accepted; remote services may work.
disconnectedThe workspace exists but its connection is down.
review-requiredA reconnected host is waiting for the user to accept it.

capabilities lists what the provider offers, using the same names as permissions where they overlap: terminal, files.read, files.edit, files.create, files.manage, files.move, files.copy, files.folders, files.upload, files.download, host.settings.

Two topics are published: system.environment carries a fresh AppEnvironment, and system.services is a signal that the service list changed. A batch with reset: true means "this is the current state", not a replay of history.

Examples ​

Render from the environment, and keep rendering as it changes:

ts
function apply(env: AppEnvironment): void {
  const connected = env.connection === "connected";
  browseButton.disabled = !connected || !env.capabilities.includes("files.read");
  shellButton.disabled = !connected || !env.capabilities.includes("terminal");
  hostLabel.textContent = env.host ? `${env.host.name} · ${env.host.system}` : "No host";
}

apply(await client.environment.get());

const unsubscribe = client.events.subscribe(
  (batch) => {
    for (const event of batch.events) {
      if (event.topic === "system.environment") apply(event.value as unknown as AppEnvironment);
      if (event.topic === "system.services") void refreshMenus();
    }
  },
  (error) => console.warn("Event stream stopped", error),
);

Call unsubscribe() when your view goes away. The SDK shares one poll between all listeners, gives a late subscriber a current snapshot, and keeps running if one listener throws.

Gate a feature on a specific method rather than guessing:

ts
const methods = await client.services.list();
const canSave = methods.some((m) => m.name === "system.files.saveText" && m.granted && m.available);

granted and available answer different questions — "may I?" and "can it work?" — and your interface usually wants to distinguish them: hide what was never granted, and explain what is temporarily unavailable.

Errors ​

CodeMessageMeaning
busyAn event read is already waiting.Two subscriptions at the transport level; use one and fan out.
closedApp events have closed.The window is going away.
invalidInvalid event cursor.A cursor from another session; resubscribe.
abortedEvent wait canceled.Normal during teardown.

The onError callback receives these; the stream ends rather than retrying forever, so resubscribe if your app intends to keep running.

Lifecycle and permissions ​

The journal keeps the most recent 128 events, and each event value is bounded, so a burst of changes cannot flood your window. If your app was not listening, the next batch arrives as a reset snapshot rather than a backlog — always accept snapshots and re-render from them.

Discovery does not grant anything: an entry with granted: false tells you the permission is missing, not that you may call it. After a reconnect the binding identity changes; re-acquire locations and handles instead of reusing the ones you held.

ShellCanvas documentation