Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions apps/composer/app/state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import {
} from "./providers";
import { planGoal } from "./planning";
import { loadExperimental, saveExperimental, type ExperimentalPreferences } from "./experimental";
import { loadBlockProvenance, saveBlockProvenance } from "./block-provenance";
import { loadBlockProvenance } from "./block-provenance";
import {
bindStepSurface,
emittedActionsBySurface,
Expand Down Expand Up @@ -589,9 +589,13 @@ export function ComposerProvider({ children }: { children: ReactNode }) {
const delta = examplesDelta(base, live);
const p = createProject({ name: `My ${ref.label} project`, description: "", source: { kind: "reference", referenceId: refId } });
if (delta.length > 0) saveExamplesDelta(p.id, delta);
// Interface blocks (experimental): the session's provenance comes along too.
if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1" && Object.keys(sessionProvenance.current).length > 0) {
saveBlockProvenance(p.id, sessionProvenance.current);
// Interface blocks (experimental): the session's provenance comes along
// too, once every write already queued for it has landed, pruned against
// the examples the duplicate keeps.
if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1") {
void import("../experimental/blocks-runtime")
.then((runtime) => runtime.copyProvenance(runtime.memoryProvenanceStore(sessionProvenance), runtime.projectProvenanceStore(p.id), live))
.catch(() => undefined);
}
setProjects(listProjects());
openProject(p.id); // clears the example workspace, merges the delta
Expand Down Expand Up @@ -626,9 +630,10 @@ export function ComposerProvider({ children }: { children: ReactNode }) {
if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1") {
try {
const raw = (JSON.parse(text) as { blockProvenance?: unknown }).blockProvenance;
const examples = ((parsed.vocab.contract.examples as ExampleEntry[] | undefined) ?? []) as ExampleEntry[];
if (raw && typeof raw === "object") {
void import("../experimental/blocks-runtime")
.then((runtime) => runtime.importProvenance(runtime.projectProvenanceStore(p.id), raw))
.then((runtime) => runtime.importProvenance(runtime.projectProvenanceStore(p.id), raw, examples))
.catch(() => undefined);
}
} catch {
Expand Down Expand Up @@ -860,6 +865,14 @@ export function ComposerProvider({ children }: { children: ReactNode }) {
.then((runtime) => runtime.gcProvenance(runtime.projectProvenanceStore(p.id), live))
.catch(() => undefined);
}
} else if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1") {
// No stored project: an Example session, which keeps its provenance
// in memory. A save prunes it the same way, so "duplicate to keep"
// carries nothing stale; outside a session the map is empty.
const live = ((doc.examples as ExampleEntry[] | undefined) ?? []) as ExampleEntry[];
void import("../experimental/blocks-runtime")
.then((runtime) => runtime.gcProvenance(runtime.memoryProvenanceStore(sessionProvenance), live))
.catch(() => undefined);
}
return { savedInMemory: true as const };
}
Expand Down
117 changes: 116 additions & 1 deletion apps/composer/app/views/inventory-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useComposer } from "../state";
import { ViewHeader } from "../ui";

Expand All @@ -26,7 +27,7 @@ export function InventoryView({ onOpen }: { onOpen: () => void }) {
</span>
);

return (
const catalog = (
<>
<ViewHeader
eyebrow="Catalog"
Expand Down Expand Up @@ -101,4 +102,118 @@ export function InventoryView({ onOpen }: { onOpen: () => void }) {
</table>
</>
);
// Interface blocks (experimental): the read-only Blocks catalog follows the
// components. The default build folds this branch away and returns the
// catalog exactly as before.
if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1") {
return (
<>
{catalog}
<BlocksCatalogSection />
</>
);
}
return catalog;
}

/**
* Interface blocks (experimental; docs/blocks/PHASE-2-RELEASE-PLAN.md, §7 H3):
* every block this build bundles, read-only. Rendered only in an experimental
* build: the literal flag check above folds this away in the default build,
* and the catalog is read through the guarded import. Nothing in it can be
* pressed: a block's actions are host-owned, and Composer executes none.
*/
function BlocksCatalogSection() {
const { activeProject } = useComposer();
const projectReferenceId = activeProject?.source.kind === "reference" ? activeProject.source.referenceId : null;
type Catalog = Array<{
referenceId: string;
label: string;
reference: { name: string; dspack: string; version: string } | null;
binding: string;
sidecarStatus: string | null;
withheld?: string;
issues: string[];
blocks: Array<{ id: string; name: string; version: number; status: string; disabled: string[]; intents: string[]; actions: string[]; streamingText: string }>;
}>;
const [catalog, setCatalog] = useState<Catalog | null>(null);
useEffect(() => {
let cancelled = false;
if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1") {
void import("../../experimental/blocks-runtime")
.then(async (runtime) => {
const next = await runtime.blocksCatalog();
if (!cancelled) setCatalog(next);
})
.catch(() => undefined);
}
return () => {
cancelled = true;
};
}, []);
const cell = { padding: "6px 8px", verticalAlign: "top" } as const;
return (
<section aria-labelledby="blocks-catalog-h" data-testid="blocks-catalog" style={{ marginTop: "clamp(32px,5vw,52px)" }}>
<h2 id="blocks-catalog-h" className="af-h2" style={{ marginBottom: 6 }}>
Interface blocks
</h2>
<p style={{ fontSize: 13, color: "var(--fg-body)", maxWidth: 720, margin: "0 0 12px" }}>
Every block this experimental build bundles, read-only. A block&rsquo;s declared actions are host-owned: Composer never executes them,
and a host binds and authorizes each one on its own server.
</p>
{catalog === null && <p style={{ fontSize: 12, color: "var(--fg-dim)" }}>Reading the blocks…</p>}
{(catalog ?? []).map((ref) => {
const headingId = `blocks-catalog-${ref.referenceId}-h`;
return (
<div key={ref.referenceId} data-testid={`blocks-catalog-ref-${ref.referenceId}`} style={{ marginTop: 18 }}>
<h3 id={headingId} style={{ fontSize: 14, margin: "0 0 4px", color: "var(--fg)" }}>
{ref.reference ? `${ref.reference.name} ${ref.reference.version}` : ref.label}
{ref.referenceId === projectReferenceId && <span style={{ fontWeight: 400, color: "var(--fg-body)" }}> · this project&rsquo;s reference</span>}
</h3>
<p style={{ fontSize: 12, color: "var(--fg-body)", margin: "0 0 6px" }}>
Sidecar {ref.sidecarStatus ?? "unreadable"} · binding {ref.binding}
{ref.withheld && <> · {ref.withheld}</>}
</p>
{ref.issues.map((issue) => (
<p key={issue} style={{ fontSize: 12, color: "var(--err)", margin: "0 0 6px" }}>
{issue}
</p>
))}
{ref.blocks.length > 0 && (
<table aria-labelledby={headingId} style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
<thead>
<tr style={{ textAlign: "left", fontFamily: "var(--mono)", fontSize: 11, textTransform: "uppercase", color: "var(--fg-body)" }}>
<th scope="col" style={cell}>Block</th>
<th scope="col" style={cell}>Version</th>
<th scope="col" style={cell}>Status</th>
<th scope="col" style={cell}>Intents</th>
<th scope="col" style={cell}>Declared actions (host-owned)</th>
<th scope="col" style={cell}>Streaming policy</th>
</tr>
</thead>
<tbody>
{ref.blocks.map((b) => (
<tr key={b.id} style={{ borderTop: "1px solid var(--line-soft)" }} data-testid={`blocks-catalog-${ref.referenceId}-${b.id}`}>
<th scope="row" style={{ ...cell, textAlign: "left", fontWeight: 400 }}>
<span style={{ fontFamily: "var(--mono)", color: "var(--fg)" }}>{b.id}</span>
<span style={{ display: "block", fontSize: 12, color: "var(--fg-body)" }}>{b.name}</span>
</th>
<td style={{ ...cell, fontFamily: "var(--mono)", fontSize: 12 }}>v{b.version}</td>
<td style={{ ...cell, fontSize: 12 }}>
{b.status}
{b.disabled.length > 0 && <span style={{ display: "block", color: "var(--err)" }}>disabled at load: {b.disabled.join("; ")}</span>}
</td>
<td style={{ ...cell, fontFamily: "var(--mono)", fontSize: 12 }}>{b.intents.join(", ")}</td>
<td style={{ ...cell, fontFamily: "var(--mono)", fontSize: 12 }}>{b.actions.join(", ")}</td>
<td style={{ ...cell, fontSize: 12 }}>{b.streamingText}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
})}
</section>
);
}
150 changes: 123 additions & 27 deletions apps/composer/app/views/preview-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,33 +318,47 @@ export function PreviewView({ focus }: { focus?: "flows" } = {}) {

/** The step's surface through the IDENTICAL single-surface path (same
* canvas, same key pattern, same registry scope) — used by both modes. */
const renderSurface = (surf: NonNullable<typeof active>) => (
<>
{!isExample && referenceExampleIds?.has(surf.name) && (
<p style={{ fontSize: 12, color: "var(--fg-dim)", margin: "0 0 6px" }}>
Reference surface from {manifest?.name ?? "the design system"} — teaching material, not part of your project&rsquo;s work.
</p>
)}
<div
{...canvasScopeFor(activeRegistryId, canvasMode).attrs}
data-project-canvas="composer"
style={{
border: "1px solid var(--line)",
borderRadius: 4,
padding: 20,
background: canvasScopeFor(activeRegistryId, canvasMode).background,
}}
>
<A2uiCanvas
key={`${surf.name}:${activeRegistryId}`}
catalog={catalog}
registry={registry}
messages={surf.messages!}
onAction={handleAction}
/>
</div>
</>
);
const renderSurface = (surf: NonNullable<typeof active>) => {
const rendered = (
<>
{!isExample && referenceExampleIds?.has(surf.name) && (
<p style={{ fontSize: 12, color: "var(--fg-dim)", margin: "0 0 6px" }}>
Reference surface from {manifest?.name ?? "the design system"} — teaching material, not part of your project&rsquo;s work.
</p>
)}
<div
{...canvasScopeFor(activeRegistryId, canvasMode).attrs}
data-project-canvas="composer"
style={{
border: "1px solid var(--line)",
borderRadius: 4,
padding: 20,
background: canvasScopeFor(activeRegistryId, canvasMode).background,
}}
>
<A2uiCanvas
key={`${surf.name}:${activeRegistryId}`}
catalog={catalog}
registry={registry}
messages={surf.messages!}
onAction={handleAction}
/>
</div>
</>
);
// Interface blocks (experimental): what the surface's block provenance
// means, above it. The default build folds this branch away and renders
// the surface exactly as before.
if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1") {
return (
<>
<BlockProvenanceNote surfaceName={surf.name} />
{rendered}
</>
);
}
return rendered;
};

return (
<section>
Expand Down Expand Up @@ -693,3 +707,85 @@ export function PreviewView({ focus }: { focus?: "flows" } = {}) {
</section>
);
}

/**
* Interface blocks (experimental; docs/blocks/PHASE-2-RELEASE-PLAN.md, §7 H3
* and §8): what this surface's block provenance means here, and, for a block
* surface, that its actions are host-owned. Rendered only in an experimental
* build: the literal flag check above folds it away in the default build, and
* the verdict is read through the guarded import. Only exact expansion
* identity earns "made with"; a verdict grants no action anything, and
* Preview still only records what a surface dispatches.
*/
function BlockProvenanceNote({ surfaceName }: { surfaceName: string }) {
const { activeProject, contract, blockProvenanceFor } = useComposer();
type Note = { block: string; version: number; madeWith: boolean; status: string; reasonText?: string };
const [note, setNote] = useState<Note | null>(null);
useEffect(() => {
let cancelled = false;
let unsubscribe = () => {};
setNote(null);
if (process.env.NEXT_PUBLIC_COMPOSER_EXPERIMENTAL === "1" && activeProject) {
const project = activeProject;
const source = project.source;
const examples = (contract?.examples as Array<{ id?: unknown; surface?: unknown }> | undefined) ?? [];
void import("../../experimental/blocks-runtime")
.then((runtime) => {
// Read now, and again after every provenance write (an accept, a
// prune, an import or a duplicate's copy can land after this mounts);
// only the latest read may set the note.
let reads = 0;
const read = async () => {
const seq = ++reads;
const provenance = blockProvenanceFor(project.id);
const verdicts = Object.hasOwn(provenance, surfaceName)
? await runtime.provenanceVerdicts({
provenance: { [surfaceName]: provenance[surfaceName] },
examples,
project: source.kind === "reference" ? { kind: "reference", referenceId: source.referenceId } : { kind: source.kind },
})
: {};
if (cancelled || seq !== reads) return;
const verdict = verdicts[surfaceName];
setNote(verdict ? { ...verdict, ...(verdict.reason ? { reasonText: runtime.PROVENANCE_REASON_TEXT[verdict.reason] } : {}) } : null);
};
if (cancelled) return;
unsubscribe = runtime.onProvenanceChange(() => void read().catch(() => undefined));
return read();
})
.catch(() => undefined);
}
return () => {
cancelled = true;
unsubscribe();
};
}, [activeProject, contract, surfaceName, blockProvenanceFor]);
if (!note) return null;
const label = `block ${note.block} v${note.version}`;
return (
<div style={{ margin: "0 0 8px", maxWidth: 720 }}>
<p data-testid="preview-provenance" style={{ fontSize: 12, color: "var(--fg-body)", margin: "0 0 4px" }}>
{note.madeWith ? (
<>
Made with {label}.{" "}
{note.status === "refinable"
? "Refinable: its record verifies against this build's approved block."
: note.status === "unavailable"
? `Unavailable: ${note.reasonText}.`
: `Inert: ${note.reasonText}.`}
</>
) : (
<>
Block provenance inert: {note.reasonText} (the record names {note.block} v{note.version}). This surface previews as an ordinary one.
</>
)}
</p>
{note.madeWith && (
<p data-testid="preview-host-owned" style={{ fontSize: 12, color: "var(--fg-body)", margin: 0 }}>
Block actions are host-owned and not executed here: Composer records each one it dispatches, and a host binds and authorizes it on its
own server.
</p>
)}
</div>
);
}
Loading
Loading