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
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:
| Value | Meaning |
|---|---|
local | The local desktop workspace. No host at all. |
connected | A host is connected and accepted; remote services may work. |
disconnected | The workspace exists but its connection is down. |
review-required | A 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:
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:
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
| Code | Message | Meaning |
|---|---|---|
busy | An event read is already waiting. | Two subscriptions at the transport level; use one and fan out. |
closed | App events have closed. | The window is going away. |
invalid | Invalid event cursor. | A cursor from another session; resubscribe. |
aborted | Event 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.
Related guides
- App lifecycle and services — permissions, availability and cleanup.
- Custom services — the discovery entries adapters add.
- Files API — the bindings and capabilities that appear here.
- Connection and workspace model — what changes underneath these states.