Interfaces
ActivityActionItem
A single action item — a mechanical fact that invites a decision.
Properties
| Property | Type | Description |
|---|---|---|
key | string | Stable key. |
meta? | string | Sub-line — attribution / cause. |
rows | ActivityRow[] | The rows this item summarizes — for drill-in / linking into the matching Activity band (items LINK to bands, they don’t duplicate). |
title | string | The headline (mechanical: “4 writes to /notes were denied”). |
type | "denied" | What it is — drives data-pyric-action-type. Today: denied. |
ActivityActionItemsProps
Properties
| Property | Type | Description |
|---|---|---|
className? | string | - |
digest | ActivityDigest | - |
emptyState? | ReactNode | Rendered when there are no action items — default renders nothing (calm by default; the region collapses). |
renderAction? | (item: ActivityActionItem) => ReactNode | Render the action button/affordance for an item (e.g. a “Debug” link). Returns null to render no action. |
renderTitle? | (item: ActivityActionItem) => ReactNode | Override item-title composition (host owns app-semantic copy). |
ActivityAuthState
Identity in effect for an op. null is anonymous / signed out.
Properties
ActivityBand
One category band: a header (label · count · attribution) plus its
rows. Mirrors the mock’s .band + .r.data structure.
Extended by
Properties
| Property | Type | Description |
|---|---|---|
attribution? | string | A short attribution phrase when the band has a dominant actor / subject (all by alice, by the app, by agent atlas). Absent when attribution is mixed — the header then shows just the count. |
count | number | Total rows in the band (== rows.length; explicit for the header). |
key | ActivityBandKey | - |
label | string | Display label — Denied, Added, Updated, … |
rows | ActivityRow[] | - |
ActivityBandWithGroups
A band that may carry pivot sub-groups. subgroups is populated only
when the digest was computed with groupBy !== 'none'; otherwise the
band is flat (rows only). computeActivityDigest always returns
bands of this shape so consumers can branch on subgroups presence.
Extends
Properties
| Property | Type | Description |
|---|---|---|
attribution? | string | A short attribution phrase when the band has a dominant actor / subject (all by alice, by the app, by agent atlas). Absent when attribution is mixed — the header then shows just the count. |
count | number | Total rows in the band (== rows.length; explicit for the header). |
key | ActivityBandKey | - |
label | string | Display label — Denied, Added, Updated, … |
rows | ActivityRow[] | - |
subgroups? | ActivitySubgroup[] | Present only when groupBy !== 'none'. |
ActivityDigest
The banded digest — the activity grid’s entire model. Bands are
pre-sorted lead-with-consequence; denials is a flat projection of
the consequential rows for the action-items tier.
Properties
| Property | Type | Description |
|---|---|---|
bands | ActivityBandWithGroups[] | Bands in render order — highest-consequence first. Empty bands omitted. Each band may carry subgroups when groupBy !== 'none'. |
denials | ActivityRow[] | The denial rows, flat + recency-sorted — the source for the action-items tier (“4 writes to /notes were denied”). A projection, not a separate aggregation: these rows also live in the denied band. |
deniedCount | number | Of those, how many were denials. |
total | number | Total rows across all bands (events the digest categorized). |
ActivityDigestOptions
Extended by
Properties
| Property | Type | Description |
|---|---|---|
groupBy? | ActivityGroupBy | Pivot rows within each band by an attribution axis. 'none' (default) leaves the band flat. Any other value splits each band’s rows into sub-groups keyed by that axis — surfaced on the row via groupKey and exposed as band.subgroups. The flat rows are always present regardless. |
order? | "recency" | "chronological" | Row order within a band. recency (default) is newest-first to match the mock’s when column. chronological is oldest-first. |
rowsPerBand? | number | Cap rows kept per band (the mock shows ~3–4 then “N more”). The digest keeps ALL rows but records the overflow via band.count vs rows.length. Set to a number to actually trim rows; the header count still reflects the true total. Default: keep all. |
ActivityGridProps
Properties
| Property | Type | Description |
|---|---|---|
className? | string | - |
digest? | ActivityDigest | A precomputed digest from useActivityDigest / computeActivity Digest. Pass this when the host already memoizes the fold (so the grid doesn’t recompute). Mutually exclusive with events. |
emptyState? | ReactNode | Shown when the digest has no rows. |
events? | readonly AnyActivityEvent[] | The unified event stream — sandbox.history() and/or a live buffer (see useActivityStream). The grid folds it into the banded digest internally. Mutually exclusive with digest. |
formatWhen? | (at: number, now: number) => string | Override the when column rendering. |
maxRowsPerBand? | number | Clamp rows shown per band; the rest collapse into a “N more” stub. Independent of the reducer’s rowsPerBand (which trims the data) — this is a pure display clamp that keeps the true count visible. Default: show all rows. |
now? | number | ”Now” anchor for formatWhen. |
onSelect? | (row: ActivityRow) => void | - |
options? | ActivityDigestOptions & { now?: number; } | Grouping / ordering options, forwarded to the reducer when the grid folds events itself. Ignored when digest is supplied. |
renderBandMore? | (band: ActivityBandWithGroups, hidden: number) => ReactNode | Render the per-band overflow stub. Default renders a [data-pyric-band-more] element reading “N more {label}”. |
selectedId? | string | The selected row id (data-pyric-selected). |
showColumnHeader? | boolean | Render a leading column-header row (target change for lens when). Default true — matches the mock’s .colhead. |
ActivityGridRowProps
Properties
| Property | Type | Description |
|---|---|---|
className? | string | - |
formatWhen? | (at: number, now: number) => string | Override the when rendering. Receives (at, now); default is the session-relative duration. |
now? | number | The “now” anchor passed to formatWhen (and row.when is used as a fallback when omitted). |
onSelect? | (row: ActivityRow) => void | - |
row | ActivityRow | - |
selected? | boolean | Marks the row as the active selection (data-pyric-selected). |
ActivityProvenance
Provenance carried by every event. All optional + additive — pre- provenance emitters omit them (read as firestore / app / app-session).
Extended by
Properties
| Property | Type | Description |
|---|---|---|
actor? | ActivityActor | - |
authLens? | ActivityLens | - |
planId? | string | Set when the op is part of an agent plan (dry-run / accept). |
service? | ActivityService | - |
ActivityRequestEvent
A Firestore request event — one per evaluated op. The digest reads
these for denials (result === 'deny') and, when no write event is
present, for the allow trail. Structurally a subset of RequestEvent.
Extends
Properties
| Property | Type | Description |
|---|---|---|
actor? | ActivityActor | - |
at | number | - |
auth | ActivityAuthState | - |
authLens? | ActivityLens | - |
groupId? | string | - |
id | string | - |
kind | "request" | - |
matchedRule? | { operations: string[]; ruleIndex: number; } | - |
matchedRule.operations | string[] | - |
matchedRule.ruleIndex | number | - |
method | "delete" | "get" | "list" | "create" | "update" | "set" | - |
origin | "user" | "transaction" | "listener" | "batch" | - |
path | string | - |
planId? | string | Set when the op is part of an agent plan (dry-run / accept). |
reasons | string[] | Simulator debug trail. Used to surface the deciding rule on denials. |
request? | { resourceData?: Record<string, unknown>; } | - |
request.resourceData? | Record<string, unknown> | - |
resourceAfter? | { data: Record<string, unknown>; exists: boolean; } | - |
resourceAfter.data | Record<string, unknown> | - |
resourceAfter.exists | boolean | - |
resourceBefore? | { data: Record<string, unknown>; exists: boolean; } | - |
resourceBefore.data | Record<string, unknown> | - |
resourceBefore.exists | boolean | - |
result | ActivityResult | - |
service? | ActivityService | - |
triggeredBy? | { method: string; path: string; } | - |
triggeredBy.method | string | - |
triggeredBy.path | string | - |
ActivityRow
A single grid row — the projection of one event onto the
target · change · for · lens · when column contract from
c-result.html. All display-ready strings plus the structured
provenance the host may style on.
Properties
| Property | Type | Description |
|---|---|---|
actor | ActivityActor | - |
at | number | Date.now() at the op. Drives the when column + recency sort. |
authLens | ActivityLens | - |
band | ActivityBandKey | Which band this row was sorted into. |
change | string | change — a short human description of the mutation (update, owner rule, done → true, created, signed in). |
denied | boolean | True for denials — first-class so hosts flag the row distinctly. |
event | ActivityEvent | The original event, for drill-in. |
for | string | for — the subject the op acted on behalf of: request.auth.uid for firestore, the affected uid for auth, else the acting identity. Empty when anonymous / not applicable. |
groupKey? | string | The pivot key this row fell under — set only when the digest was computed with groupBy !== 'none'. Mirrors the row’s subgroup. |
id | string | The originating event’s id — stable React key. |
lens | string | lens — the privilege the op ran under, display-ready (app, as alice, admin). |
planId? | string | Agent plan id, when the op was part of a plan. |
service | ActivityService | Originating service. |
subjectUid | string | The on-behalf-of subject uid, structured (mirrors for). |
target | string | target — what was mutated, in the service’s addressing scheme (a doc path notes/3agHoZHZ, a uid, a storage fullPath, an rtdb path). Empty string when the event has no addressable target. |
when | string | when — left undterived here; the grid formats at itself, but a pre-rendered relative string is offered for headless consumers. |
ActivityServiceMutationEvent
Cross-service mutation — the unified envelope auth / storage / rtdb
emit. The digest maps service + op to a band (signed-in, added,
updated, removed, …). Subset of ServiceMutationEvent.
Extends
Properties
| Property | Type | Description | Overrides |
|---|---|---|---|
actor? | ActivityActor | - | - |
after? | unknown | - | - |
at | number | - | - |
auth | ActivityAuthState | - | - |
authLens? | ActivityLens | - | - |
before? | unknown | - | - |
detail? | Record<string, unknown> | - | - |
id | string | - | - |
kind | "service_mutation" | - | - |
op | string | - | - |
path? | string | - | - |
planId? | string | Set when the op is part of an agent plan (dry-run / accept). | - |
service | "rtdb" | "auth" | "storage" | - | ActivityProvenance.service |
ActivitySubgroup
Properties
| Property | Type |
|---|---|
count | number |
key | string |
rows | ActivityRow[] |
ActivityWriteEvent
A committed Firestore write — create/update/set/delete that
the rule engine allowed and the keyspace applied. The digest leads
with these for the added/updated/removed bands (richer than request:
carries prior/next state for the change summary). Subset of
WriteSandboxEvent.
Extends
Properties
| Property | Type | Description |
|---|---|---|
actor? | ActivityActor | - |
at | number | - |
auth | ActivityAuthState | - |
authLens? | ActivityLens | - |
data? | Record<string, unknown> | - |
groupId? | string | - |
id | string | - |
kind | "write" | - |
method | "delete" | "create" | "update" | "set" | - |
nextState | Record<string, unknown> | - |
path | string | - |
planId? | string | Set when the op is part of an agent plan (dry-run / accept). |
priorState | Record<string, unknown> | - |
service? | ActivityService | - |
CreatedAuthUser
An auth user a staged proposal creates (a sign-in account, distinct from
Firestore documents). The host adapts its backend request (e.g. a
CreateUserRequest) into this UI-level shape.
Properties
FieldChange
One field-level change in a staged proposal. A UI-level diff row: the host
adapts its backend diff (e.g. the sandbox’s Divergence[]) into these so the
component stays decoupled from any backend type.
Properties
ProposedChangeDiffProps
Properties
| Property | Type | Description |
|---|---|---|
authUsers? | readonly CreatedAuthUser[] | Auth users the proposal creates, shown as a leading “auth users” group. |
changes | FieldChange[] | - |
className? | string | - |
emptyState? | ReactNode | Rendered when there are no changes. Defaults to nothing. |
formatValue? | (value: unknown) => ReactNode | Format a value for display. Default: JSON-ish, empty for undefined. |
UseActivityDigestOptions
Extends
Properties
| Property | Type | Description |
|---|---|---|
groupBy? | ActivityGroupBy | Pivot rows within each band by an attribution axis. 'none' (default) leaves the band flat. Any other value splits each band’s rows into sub-groups keyed by that axis — surfaced on the row via groupKey and exposed as band.subgroups. The flat rows are always present regardless. |
now? | number | Clock injection for the relative when column. Defaults to Date.now() read once per recompute. Pass a fixed value (or a frozen “session now”) for deterministic rendering / tests. NOTE: changing now between renders re-folds the digest, so don’t pass a fresh Date.now() inline unless you want a recompute every render — pin it (e.g. a ticking value updated on an interval). |
order? | "recency" | "chronological" | Row order within a band. recency (default) is newest-first to match the mock’s when column. chronological is oldest-first. |
rowsPerBand? | number | Cap rows kept per band (the mock shows ~3–4 then “N more”). The digest keeps ALL rows but records the overflow via band.count vs rows.length. Set to a number to actually trim rows; the header count still reflects the true total. Default: keep all. |
UseActivityStreamOptions
Properties
| Property | Type | Description |
|---|---|---|
bufferSize? | number | Ring-buffer cap. Once exceeded, the oldest events drop. Default 5000 (mirrors the traffic monitor’s cap — a load-test session can emit 100k+ events). |
initial? | readonly AnyActivityEvent[] | Seed the buffer with a snapshot before the live subscription attaches — typically sandbox.history(), so a late-attaching consumer sees the whole session, not just events from subscribe onward. Read once on mount. |
paused? | boolean | Start paused — incoming events drop (not queued) while paused. |
source | ActivitySource | The subscription — sandbox.onEvent satisfies this directly. Pass a stable reference; the hook re-subscribes on identity change. |
UseActivityStreamResult
Properties
| Property | Type | Description |
|---|---|---|
clear | () => void | Empty the buffer (does NOT re-seed from initial). |
events | AnyActivityEvent[] | The buffered events, oldest-first. Stable per emission — safe to hand straight to useActivityDigest. |
isPaused | boolean | - |
pause | () => void | - |
resume | () => void | - |
Type Aliases
ActivityActor
type ActivityActor =
| {
kind: "app";
}
| {
kind: "studio";
}
| {
kind: "agent";
name: string;
}
| {
kind: "app-builder";
};
Who initiated the op behind an event. Absent ⇒ the served app.
ActivityBandKey
type ActivityBandKey =
| "denied"
| "errored"
| "added"
| "updated"
| "removed"
| "signed-in"
| "signed-out"
| "read"
| "other";
The band a row falls into. denied leads; the write bands
(added/updated/removed) and the auth bands (signed-in/signed-out)
follow; read, errored, and other are the long-tail catch-alls.
denied— a rules denial (firestorerequestwithresult: 'deny').errored— an operational failure (requestwithresult: 'unsupported').added— a doc/object created, or a user created.updated— a doc/object/user/rtdb path mutated in place.removed— a doc/object/user deleted.signed-in— an auth sign-in.signed-out— an auth sign-out.read— an allowed firestore read (get/list).other— anything modelled but uncategorized (e.g. an unknown service op).
ActivityEvent
type ActivityEvent =
| ActivityRequestEvent
| ActivityWriteEvent
| ActivityServiceMutationEvent;
The union members the activity digest aggregates. This is a SUBSET of
pyric’s SandboxEvent — the listener-lifecycle / snapshot-delivery /
session-boundary kinds carry no user-visible mutation, so the digest
ignores them. The reducer accepts any object with a kind and skips
the kinds it doesn’t model, so a full SandboxEvent[] passes through
cleanly (the extra kinds fall into the “unknown ⇒ skipped” branch).
ActivityGroupBy
type ActivityGroupBy = "none" | "actor" | "lens" | "subject" | "service";
How rows within each band are pivoted/grouped.
ActivityLens
type ActivityLens =
| {
mode: "admin";
}
| {
mode: "as";
uid: string;
}
| {
mode: "app-session";
};
The auth lens an op ran under. admin bypasses rules, as evaluates
rules as a specific uid (impersonation), app-session is the app’s
own signed-in user. Absent ⇒ app-session.
ActivityResult
type ActivityResult = "allow" | "deny" | "unsupported";
Firestore op outcome — present on request events only.
ActivityService
type ActivityService = "firestore" | "auth" | "storage" | "rtdb";
Which sandbox service emitted an event. Absent ⇒ 'firestore'.
ActivitySource()
type ActivitySource = (cb: (event: AnyActivityEvent) => void) => () => void;
A subscription: register a callback, get an unsubscribe.
sandbox.onEvent matches this signature.
Parameters
| Parameter | Type |
|---|---|
cb | (event: AnyActivityEvent) => void |
Returns
(): void;
Returns
void
AnyActivityEvent
type AnyActivityEvent =
| ActivityEvent
| {
[k: string]: unknown;
kind: string;
};
The widened input the reducer accepts: any ActivityEvent, plus a
permissive escape for unmodelled SandboxEvent kinds carrying a
string kind (skipped). Lets sandbox.history() flow in unfiltered.
Functions
ActivityActionItems()
function ActivityActionItems(__namedParameters: ActivityActionItemsProps): ReactNode;
The action-items tier — the few things wanting a decision, surfaced
ABOVE the activity grid (design-ideation Tier 2 / “Needs you”).
Denials lead and are first-class. Mechanical copy by default; the host
supplies the action affordance (e.g. a “Debug” link into the rules
debugger) via renderAction.
Data contract:
[data-pyric-ui="activity-action-items"]— the root (absent when empty and noemptyState).[data-pyric-action-item](+data-pyric-action-type,data-pyric-action-count) — one item.[data-pyric-action-title]/[data-pyric-action-meta]— the copy.[data-pyric-action-affordance]— wraps the host’s action node.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ActivityActionItemsProps |
Returns
ReactNode
ActivityGrid()
function ActivityGrid(__namedParameters: ActivityGridProps): ReactNode;
Headless activity grid over the unified SandboxEvent stream. Folds
events into category bands (Denied / Added / Updated / Removed /
Signed in / …), each band a target · change · for · lens · when
column grid grouped under a label · count · attribution header.
Denials lead (lead-with-consequence) and are flagged first-class.
Ships ZERO styling — the host (Pyric Studio) applies the rigid column
grid + band typography via the data-pyric-* contract:
[data-pyric-ui="activity-grid"]— the root.[data-pyric-band]— a band header, withdata-pyric-band-key,data-pyric-band-count,data-pyric-band-denied(on the denied band). Children:[data-pyric-band-label],[data-pyric-band-n],[data-pyric-band-attr](omitted when attribution is mixed).[data-pyric-band-rows]— the row container; with grouping,[data-pyric-band-subgroup](+data-pyric-subgroup-key) wraps each pivot bucket.[data-pyric-band-more]— the “N more” overflow stub.- the
<ActivityGridRow>contract for each row.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ActivityGridProps |
Returns
ReactNode
ActivityGridRow()
function ActivityGridRow(__namedParameters: ActivityGridRowProps): ReactNode;
One activity grid row — the target · change · for · lens · when
column contract from c-result.html, plus a trailing drill affordance
column. Headless: every cell is a data-pyric-event-* span the host
styles into the rigid column grid.
Styling / data contract:
[data-pyric-event-row]— the row, withdata-pyric-event-band,data-pyric-event-service,data-pyric-event-lens,data-pyric-event-denied(present only on denials), anddata-pyric-selectedwhen active.[data-pyric-event-target]/-change/-for/-lens/-when— the five columns, in order.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ActivityGridRowProps |
Returns
ReactNode
computeActivityDigest()
function computeActivityDigest(events: readonly AnyActivityEvent[], opts?: ActivityDigestOptions & {
now?: number;
}): ActivityDigest;
Fold a unified event stream into the banded activity digest. PURE —
no React, no clock reads except the injected now (defaults to
Date.now(), but pass a fixed value in tests for determinism).
Events the digest doesn’t model (listener lifecycle, snapshot
delivery, session boundaries, unknown kinds) are skipped — a full
SandboxEvent[] from sandbox.history() flows in unfiltered.
Parameters
| Parameter | Type |
|---|---|
events | readonly AnyActivityEvent[] |
opts? | ActivityDigestOptions & { now?: number; } |
Returns
defaultFormatWhen()
function defaultFormatWhen(at: number, now?: number): string;
Default when-column formatter: a session-relative duration
(now / 12s / 3m / 1h), matching the mock’s c-when strings.
The grid is session-scoped (see design-ideation “FRAME CORRECTION”)
so anchors are relative, never absolute calendar dates.
Override via <ActivityGrid formatWhen={...} /> when the host has a
better clock anchor (e.g. a ticking “session now”).
Parameters
| Parameter | Type |
|---|---|
at | number |
now? | number |
Returns
string
ProposedChangeDiff()
function ProposedChangeDiff(__namedParameters: ProposedChangeDiffProps): Element;
Headless renderer for a staged change: the documents a proposal touches, with
per-field before/after. Grouped by collection. Ships zero styling; the host
styles the data-pyric-* contract (proposed-change-diff /
data-pyric-change-*). The c-review diff grid is this, styled.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ProposedChangeDiffProps |
Returns
Element
useActivityDigest()
function useActivityDigest(events: readonly AnyActivityEvent[], options?: UseActivityDigestOptions): ActivityDigest;
React wrapper over computeActivityDigest — memoizes the pure
fold over the unified SandboxEvent stream into the banded activity
digest. Feed it sandbox.history() (a snapshot) or the live buffer
from useActivityStream; the reducer is identical either way.
The fold re-runs when events identity, any grouping option, or
now changes. Keep events referentially stable across renders that
shouldn’t recompute (the stream hook already returns a stable array
per emission).
Parameters
| Parameter | Type |
|---|---|
events | readonly AnyActivityEvent[] |
options? | UseActivityDigestOptions |
Returns
useActivityStream()
function useActivityStream(__namedParameters: UseActivityStreamOptions): UseActivityStreamResult;
Buffers the unified sandbox event stream into a capped ring buffer
with optional history seeding + pause/resume/clear. Decoupled from
pyric — source is just a (cb) => unsubscribe.
Sibling to the traffic monitor’s useTrafficMonitor; the difference
is the wider event type (AnyActivityEvent, the full union) and the
initial seed so history() + live compose into one buffer.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | UseActivityStreamOptions |