@granular-software/react
v0.1.0
Published
Customer-facing React components and hooks for Granular browser sessions and frontend actions
Downloads
150
Readme
@granular-software/react
React bindings for delegated Granular browser sessions, frontend actions, and embedded agent UI.
This package is designed for customer-facing product surfaces:
- your backend authenticates the current end user
- one small server route exchanges that stable user ID for a short-lived Granular token
GranularProviderrefreshes that token through the route when needed- the browser opens a subject-scoped Granular session
- frontend actions are published from that browser session only
GranularProvider accepts either a trusted exact environmentId or an
environmentName (defaulting to prod). For a name-only exchange, the gateway
first verifies the server's scoped Embedded UI key, reconciles the subject,
checks its sandbox assignment, and resolves the named environment that your
backend or the Granular console already provisioned.
The browser token is then bound to the resulting exact environment ID; an
environment name is never used as an authorization boundary.
Install
bun add @granular-software/react @granular-software/sdkCore Split
Customer backend
- owns user authentication
- owns the
/api/granular/sessionroute - owns manifest generation, ontology setup, and backend action execution
- may also own the
agentEndpoint
Customer frontend
- mounts
GranularProviderorGranularWidget - publishes frontend actions for the current browser surface
- renders the canonical feed and its prompt/artifact/file/resource controls
- may inspect tasks, timeline, and jobs through diagnostic hooks, but never merges those collections into customer-visible chronology
Customer-facing copy
The shared components own connection, recovery, and generic failure copy. Your
app owns the product vocabulary and deliberate business explanations, such as a
policy result or a field that needs changing. Do not render raw backend errors
in product UI. For configuration failures, pass the stable platform code to
GranularAgentDockErrorState and let the Dock render its own error state.
Provide clear business labels and descriptions for your actions, prompts, fields, results, and feedback. The Dock deliberately falls back to neutral language instead of guessing what an internal identifier or effect name means.
See Customer-Facing Copy And Ownership for the complete contract.
Main Exports
Provider and hooks
GranularProvideruseGranularSessionuseGranularAgentuseGranularSubjectuseGranularEnvironmentusePublishFrontendActionsuseGranularDocumentuseGranularFeeduseGranularHeapuseGranularTimelineuseGranularTasksuseGranularPromptsuseGranularJobsuseGranularSessionTimelineuseGranularSessionJobsuseGranularSessionJobuseGranularSessionHeapEntriesuseGranularSessionHeapEntryuseGranularSessionHeapListsuseGranularSessionHeapListuseGranularSessionTranscriptuseGranularHarnessuseGranularEffectsuseGranularHarnessStateuseGranularRecordImportSummaryuseGranularRecordImportsuseGranularRecordImportuseGranularSessionStatsuseGranularJob
UI components
GranularWidgetGranularAgentPanelGranularMessengerGranularAgentDockGranularActionBar
Minimal Setup
import {
GranularMessenger,
GranularProvider,
usePublishFrontendActions,
} from "@granular-software/react";
import type { ToolWithHandler } from "@granular-software/sdk";
const openRefundDrawer: ToolWithHandler = {
name: "open_refund_drawer",
description:
"Open the in-product refund review drawer for the selected order.",
parameters: {
type: "object",
properties: {
orderId: { type: "string" },
},
required: ["orderId"],
},
async handler(args) {
// Customer-authored UI logic goes here.
console.log("Open refund drawer for", args.orderId);
return { ok: true };
},
};
function FrontendActions() {
usePublishFrontendActions([openRefundDrawer]);
return null;
}
export function SupportDesk() {
return (
<GranularProvider
apiUrl={process.env.NEXT_PUBLIC_GRANULAR_API_URL!}
ontologyId="support-ops"
agentEndpoint="/granular/agent"
sessionKey="current-authenticated-user"
>
<FrontendActions />
<GranularMessenger />
</GranularProvider>
);
}Every custom agentEndpoint receives the stable
operationId, userMessageId, and user-message metadata after the Provider
durably appends that user occurrence. It also receives ordered conversation
history projected from the complete canonical session feed; browser-local
optimistic state is never used as history. The backend must durably author or
reuse every non-empty assistant reply in
the same Granular session and return:
const published = await environmentSession.publishAssistantReply({
id: assistantMessageId,
operationId: assistantOperationId,
text: reply,
});
return Response.json({
reply,
meta: {
durableMessagePersisted: true,
durableMessageId: published.feedItemId,
durableMessageTimestamp: published.timestamp,
},
});An unmarked reply fails closed so browser code cannot forge assistant
history. publishAssistantReply() requires a server API-key
EnvironmentSession; delegated browsers cannot call it. A custom endpoint
returns one JSON result; it has no assistant or progress stream. Chronological
components must be published to the canonical session feed and become visible
through the feed subscription. Product result fields such as patch,
recommendation, artifacts, target, and meta remain available to
onAgentResult.
Omit agentEndpoint to use the Provider's atomic Harness path.
Frontend actions still need two things:
- declaration in the ontology manifest
- live publication from the browser surface
You can publish them either:
- statically with the
frontendActionsprop onGranularProvider - dynamically from a component lifecycle with
usePublishFrontendActions(...)
Reference
The full API reference lives in REFERENCE.md.
It covers:
- delegated subject authentication
- session creation and session listing
- environment APIs such as GraphQL, record ingestion, and record-import tracking
- live CRDT hooks for heap, tasks, prompts, timeline, jobs, and harness state
- job feedback and execution-state subscriptions
- built-in UI components
Agent Dock
GranularAgentDock renders the compact bottom command surface used for
workspace chat, object-scoped follow-ups, active-session switching, and operator
attention prompts.
For the shortest Next.js integration path, see NEXTJS.md.
<GranularAgentDock />The dock ships with the canonical visual contract by default: a responsive single-row launcher, conversation depth, focused artifact workbench, session history, factual activity, inline attention prompts, unread state, object-aware context receipts, and wrapping starter suggestions. Add props only for product context:
// Shell/layout:
<GranularAgentDock
agentContext={() => ({ selectedOrder })}
objectDisplay={{
order: {
label: "Order",
href: (object) => `/orders/${object.id}`,
subtitle: (object) => object.description,
},
}}
/>;
// Object page:
useGranularTarget("order", selectedOrder.id, selectedOrder.orderNumber);If the shell already owns the current object, pageTarget is also available:
<GranularAgentDock pageTarget={currentPageObject} />The conversation primitives are not injectable. GranularAgentDock always
owns and renders its canonical shadcn Base/Nova Bubble, Message, Marker,
MessageScroller, and Attachment components. Product integrations provide
context and behavior; they cannot replace the dock's message or artifact UI.
theme and root-level layout style remain available for unusual embedding
constraints, but neither changes the component implementation.
Deterministic action autonomy
Prepared session artifacts can declare impact metadata in policy (or the
legacy metadata.autonomy field). The dock applies a fixed policy:
- declared read-only and UI-only work can run directly
- single internal mutations require a visible review
- bulk, external, financial, permission, destructive, and high-risk work requires impact review followed by explicit confirmation
- missing impact metadata uses the conservative review path; the dock never asks a model to guess the risk
const artifact = {
// ...SessionArtifactRecord fields
policy: {
sideEffect: "write",
scope: "financial",
risk: "high",
targetCount: 4,
reversible: false,
},
};Conversation activity renders from canonical feedback occurrences. The Dock does not derive a second timeline from response actions or generated reasoning prose. Drafts, attachments, prompt inputs, selected artifacts, and scroll position are restored per session when the user switches conversations.
Delegated Auth Setup
Customer backend
- creates an Embedded UI key and stores it as
GRANULAR_EMBEDDED_UI_API_KEY - exposes
POST /api/granular/session - must derive the current end user from the customer’s own trusted session
- passes only that stable user ID to Granular from the server
- must keep the Embedded UI key on the server only
- should shut down or rotate the embedded Granular session when the host user logs out or switches account
Use the server helper so request validation, safe errors, and no-store headers stay consistent:
import { createGranularSessionRoute } from "@granular-software/sdk/delegated-auth";
export const POST = createGranularSessionRoute({
getSubject: async (request) => (await readYourSession(request))?.userId,
});Typical backend config:
GRANULAR_EMBEDDED_UI_API_KEYGRANULAR_API_KEYonly when the backend also performs trusted setup, imports, or administration- your own existing customer session-cookie / auth middleware config
Granular platform
- validates that the key has only the browser-session creation permission
- resolves the tenant from the key and the subject from the stable user ID
- resolves the user's current assignment and permission profile
- returns a token scoped to one exact environment for at most five minutes
Customer frontend
- mounts
GranularProvider - passes
ontologyId;/api/granular/sessionand the hosted gateway are defaults - may pass
agentEndpoint - may pass
sessionKeywhen host-user switches should force a reconnect - may publish frontend actions with either
frontendActionsorusePublishFrontendActions - does not need a cookie private key or signing secret in the browser
Typical frontend env/config:
NEXT_PUBLIC_GRANULAR_API_URLNEXT_PUBLIC_GRANULAR_ONTOLOGY_ID- optional
NEXT_PUBLIC_GRANULAR_AGENT_ENDPOINT
Notes:
clientIdis optional; the package generates one when omitted- there is no library-mandated cookie name
- there is no frontend-managed API key, refresh token, signing key, or cookie secret
- there is no required cookie key name or cookie encryption secret specific to Granular
Recommended Hook Shape
For most apps, the cleanest mental model is:
useGranularSubject(): delegated identity plus session catalog/creationuseGranularEnvironment(): environment APIs, record ingestion, and frontend-action publicationuseGranularSession(): full low-level provider state and direct access to the active session objectuseGranularJob(job): one live execution with logs, tool calls, prompts, and feedback
The lower-level slice hooks such as useGranularHeap(), useGranularTimeline(), and useGranularTasks() still exist because the live document is CRDT-backed and they are the best React shape for focused UI surfaces.
For persisted session history and saved artifacts, use the durable hooks:
useGranularFeed()useGranularSessionTranscript()useGranularSessionTimeline()useGranularSessionJobs()useGranularSessionHeapEntries()/useGranularSessionHeapEntry()useGranularSessionHeapLists()/useGranularSessionHeapList()
useGranularFeed() is the sole customer-visible chronology. Timeline and job
hooks are diagnostics and must not be merged into the display feed.
Important Runtime Model
Manifest generation, ontology builds, and backend actions stay in your backend stack.
The frontend package is responsible for:
- delegated authentication in the browser
- opening the live browser session
- publishing frontend actions for that session
- rendering live session state with React
- answering prompts and showing agent progress in-product
