Developer

Calls

Typed RPC hooks. One name, one schema, one implementation - across every transport.

Calls are typed RPC hooks. Each is declared once in the central CallRegistry and callable from every transport: custom-applet iframes, MCP agents, automation scripts, and internal Rust code.

Capability required: hooks:consume (to call); the hook’s applet-type permission (e.g. docs:read, docs:write) governs what you can call.

Architecture

flowchart TB
  subgraph Callers["Callers (any transport)"]
    A["Custom applet iframe<br/>rp.docs.foo()"]
    B["MCP agent<br/>tool: docs.foo"]
    C["Automation script<br/>rp.docs.foo()"]
    D["Rust internal<br/>registry.invoke()"]
  end

  subgraph Core["Rust CallRegistry - single source of truth"]
    E[Permission check]
    F{Handler type?}
  end

  G["Rust handler<br/>disk / DB / config"]
  H["Frontend target<br/>mounted editor<br/>(TipTap, UI state)"]

  A --> E
  B --> E
  C --> E
  D --> E
  E --> F
  F -->|Rust| G
  F -->|Frontend| H
  H <-.->|"Tauri event<br/>+ one-shot reply"| I["Mounted component<br/>(claimed target)"]

Call names

Canonical format: <resource_type>.<camelCaseHook>. Examples:

  • docs.getOpenedFile
  • docs.insertAtSelection

The same name is used everywhere - TypeScript (rp.docs.getOpenedFile), MCP (docs.getOpenedFile), HTTP (POST /hooks/invoke with "name": "docs.getOpenedFile"), and Rust (registry.invoke("docs.getOpenedFile", ...)).

From your custom applet

import { createHooksProxy, type HookTransport } from "@rightplace/sdk";

const transport: HookTransport = {
  invoke: async (name, params) => {
    // Send to the main app via the postMessage bridge.
    return ipc.request("hooks:invoke", { name, params });
  },
};
const rp = createHooksProxy(transport);

const file = await rp.docs.getOpenedFile({ resource_id: resourceId });
// file.path, file.content, file.is_dirty, file.cursor

await rp.docs.insertAtSelection({ resource_id: resourceId, text: "hello" });

Manifest:

{
  "apiVersion": 1,
  "capabilities": ["hooks:consume", "docs:read", "docs:write"],
  "hooks": {
    "calls": ["docs.getOpenedFile", "docs.insertAtSelection"]
  }
}

Both the capabilities (coarse) and the hooks.calls allowlist (per-hook) must list the permission/hook before the call is allowed. Users see the allowlist at install time.

Error model

ErrorWhenWhat to do
UnknownHookName not in registryCheck spelling / SDK version
PermissionDeniedMissing capability or allowlistAdd to manifest, reinstall
InvalidParamsParams don’t match schemaCheck the hook’s schema
TargetNotAvailableFrontend handler, editor not mountedPrompt user to open the file, or fail
TargetTimeoutClaimed but didn’t reply in 5sRetry or fail
HandlerErrorHandler threwPass-through error message

Discovery

// From the main app, via Tauri:
const hooks = await invoke<Array<{ name: string; description: string }>>("cmd_hook_list");
const searchResults = await invoke("cmd_hook_search", { query: "highlight text" });

Keyword-match over name + description. Vector search is planned.

Position semantics

Hooks that take or return positions (docs.moveCursor, docs.setSelection, etc.) use UTF-16 code-unit offsets into the applet’s serialized text (e.g., markdown source). Matches JavaScript’s native string indexing.

Catalog

Current applets that expose call-hooks:

  • docs.* - docs editing and search (9 hooks)
  • browser.* - agent control of the in-app CEF browser (20 hooks)