@rectsh/rect
v0.8.4
Published
Rect — the state store views embed to sync a view model with the Rect host, plus the Vite plugin (./vite) that compiles a view to a deployable bundle.
Readme
@rectsh/rect
The SDK for building Rect views — small interactive apps that bind to a server-side JSON store and sync every edit. An agent (or a person) issues a view, a human opens it at a capability URL, and both sides see live updates.
A view is an HTML bundle containing its UI code and assets. You can hand-write it, or author it in React with this SDK. The host injects the shared, versioned Rect protocol runtime when it serves the view.
SDK projects can also include rect.agent.md. The Vite plugin publishes its
Markdown as the agent instructions for creating and updating the Rect, while
rect.view.json's description stays a short explanation of when to use it.
npm i @rectsh/rect
npm i -D viteQuickstart (React)
// src/main.tsx
import { createRoot } from 'react-dom/client';
import { RectProvider, useRectState, useRectField } from '@rectsh/rect/react';
interface Note {
title: string;
body: string;
}
function Editor() {
const state = useRectState<Note>();
const [body, setBody] = useRectField<string>('body');
return (
<main>
<h1>{state.title}</h1>
<textarea value={body ?? ''} onChange={(e) => setBody(e.target.value)} />
</main>
);
}
createRoot(document.getElementById('root')!).render(
<RectProvider fallback={<p>Connecting…</p>}>
<Editor />
</RectProvider>,
);// vite.config.ts
import { defineConfig } from 'vite';
import { rect } from '@rectsh/rect/vite';
export default defineConfig({
esbuild: { jsx: 'automatic', jsxImportSource: 'react' },
plugins: [rect()],
});// rect.view.json — the self-describing spec
{
"name": "Note",
"description": "Use when an agent and a person need to draft a shared note.",
"stateSchema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"body": { "type": "string" }
},
"required": ["title", "body"],
"additionalProperties": false
},
"example": { "title": "Untitled", "body": "" }
}pnpm dev— develop in the browser with HMR against a mock host (see below).pnpm build— compile to a singledist/index.htmlyou upload as a view.
The view contract
A view runs inside a sandboxed iframe with host-owned CSP. This shapes everything you build:
- Prefer self-contained UI bundles. The host-provided Rect runtime is the
one exception. Relative asset URLs work because the host serves the whole
dist/. The plugin only fails unsupported URL forms:http://and//; host CSP/sandbox decide whether runtimehttps://loads. - Use the host bridge for state. Reads and writes are relayed to the parent
host over
postMessage; any direct runtimehttps://request is governed by the host CSP/sandbox. - No same-origin storage.
localStorage/cookies are unavailable. - ≤ 20 MB compiled.
State & syncing
connect() returns a store. Reads are synchronous; writes apply optimistically,
are sent to the host as an
RFC 7386 JSON Merge Patch. Committed
changes return as granular
RFC 6902 JSON Patch operations and are
rebased onto local optimistic state.
import { connect } from '@rectsh/rect';
const rect = await connect();
rect.get(); // whole view model
rect.get('items.0.done'); // a path (numeric segments index arrays)
rect.subscribe((state) => render(state));
rect.set('title', 'Hi');
rect.update((draft) => draft.items.push(item));
rect.patch({ completion: { approved: true } });
rect.revision; // optimistic-concurrency tokenReact (@rectsh/rect/react)
| Hook | Purpose |
| --- | --- |
| <RectProvider> | Connects and provides the store; gates children until ready. Pass store to inject a mock in tests. |
| useRectState<T>() | The whole view model. Re-renders on any change. |
| useRectValue(selector, isEqual?) | A derived slice; re-renders only when it changes. |
| useRectField<V>(path) | [value, setValue] two-way binding for one path. |
| useRectActions<T>() | Stable { set, update, patch } handles. |
| useRectDispatch() | Dispatch a named action or one of the narrowly allowlisted host commands. |
| useRectAttachments() | Resolve or fetch attachment bytes by durable attachment id. |
| useRect() | The raw store (escape hatch). |
| useRectRevision() / useRectMeta() | Revision / host metadata. |
To continue an Agent-backed App from inside a View, send a person-facing message through the host:
const dispatch = useRectDispatch();
await dispatch('agent-message', 'Form filled. Start the next step.');The runtime flushes pending ViewModel edits before forwarding the message, so
the Agent reads the values the person just entered. agent-message is a
host command, not a named action; standalone Views and local mock hosts return
agent_unavailable.
The official App Maker uses a separate platform command after its
requestPublish action succeeds:
await dispatch('host-command', { command: 'publish-app-draft' });This command publishes the host-owned App Maker snapshot without starting an Agent turn. It is reserved for the official App Maker; other Rects must not use it and the host validates the bound App run, instance, revision, and caller.
Attachment credentials are runtime state, not ViewModel state. Read bytes by stable attachment id and keep signed URLs out of effect dependencies and cache keys:
const attachments = useRectAttachments();
const response = await attachments.fetch(attachmentId);Testing (@rectsh/rect/testing)
import { createMockRect } from '@rectsh/rect/testing';
const rect = createMockRect({ title: 'Draft', body: '' });
render(
<RectProvider store={rect}>
<Editor />
</RectProvider>,
);Local dev (@rectsh/rect/dev)
Develop in a normal browser with HMR against a mock host — no upload loop. A
dev.html harness embeds your view in an iframe and plays the host:
import { createRectDevHost, mountRectDevPanel } from '@rectsh/rect/dev';
import spec from './rect.view.json';
const iframe = document.querySelector('iframe')!;
const host = createRectDevHost({
iframe,
initialState: spec.example,
attachmentUploadEndpoint: '/api/rect/dev/attachments',
});
mountRectDevPanel(host, document.getElementById('panel')!);The panel lets you push state to the view exactly as an agent's patch_view
would, so you can see how your view reacts to live updates. Passing the Vite
attachment endpoint also makes useRectAttachmentUpload() support the
host-owned picker and trusted drop-event files while populating the local
$attachments registry through the same policy checks.
Hand-written views
You don't need a bundler. Any HTML that embeds the spec block and calls the
host-provided Rect global works. The vanilla examples under
public/rect-js/examples/ load the same versioned runtime for standalone local
testing. The npm package is a typed adapter around the host runtime.
Examples
Full React views built with this SDK live in
examples/:
example-dashboard-view— a compact dev-status dashboard.annotated-editor-view— a Tiptap markdown editor an agent drafts and a human annotates.attachment-upload-view— a view that renders$attachments, requests host-owned uploads, and binds a selected attachment with a named action.
Or scaffold a fresh project with npm create rect.
License
MIT
