Skip to content

feat(react-ui): allow rich content components in StepsItem details#759

Open
Shinyaigeek wants to merge 1 commit into
thesysdev:mainfrom
Shinyaigeek:feat/stepsitem-rich-details
Open

feat(react-ui): allow rich content components in StepsItem details#759
Shinyaigeek wants to merge 1 commit into
thesysdev:mainfrom
Shinyaigeek:feat/stepsitem-rich-details

Conversation

@Shinyaigeek

@Shinyaigeek Shinyaigeek commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Motivation

StepsItem's details prop is z.string() in the genui schema, which means an LLM can only express a step's body as one markdown string. Procedural content — installation guides, usage instructions — often needs real CodeBlock, Image, or MarkDownRenderer components alongside prose (as requested in user feedback asking for details to accept rich content similar to SectionItem's content).

Notably, only the schema blocks this: the React component layer already types details: React.ReactNode, and the genui renderer already has a non-string branch that renders details through renderNode:

const details = item.props.details;
const detailsContent =
typeof details === "string" ? (
<MarkDownRenderer textMarkdown={details} />
) : (
(renderNode(details) as React.ReactElement)
);

With details: z.string(), the renderNode(details) arm was unreachable dead code — the receiving side existed, only the schema kept the door shut.

Change

  • details becomes z.union([z.string(), z.array(StepsItemDetailsChildUnion)])
  • New StepsItemDetailsChildUnion = TextContent | MarkDownRenderer | CodeBlock | Image | ImageBlock. Kept intentionally narrower than SectionContentChildUnion: procedural steps need prose/code/images, referencing Steps from its own item schema would be circular, and a small union keeps the prompt signature compact. The exact set of components that should qualify as rich content is open to discussion — I kept it to prose, code, and images, but happy to adjust.
  • Renderer: only a cast relaxation (ReactElementReactNode) and a description update; the existing renderNode branch handles arrays as-is.

The prompt signature the LLM sees changes from

StepsItem(title: string, details: string)

to

StepsItem(title: string, details: string | (TextContent | MarkDownRenderer | CodeBlock | Image | ImageBlock)[])

Screenshots

Live Vite harness rendering openuiLibrary. Each page shows the prompt signature from openuiLibrary.toSpec() at the top, panel A with string details, and panel B with component details.

Before

details: string — panel B's program renders only because the runtime never validated children against the schema; the LLM never sees this form.

before: signature shows details: string

After

The signature advertises the union, and panel B exercises all five members in one Steps: TextContent (steps 1, 3), MarkDownRenderer with card variant (step 2), CodeBlock (steps 1–3), Image (step 3 diagram), ImageBlock (step 4).

after: signature shows the union and all five members render

Backward compatibility

String details still renders through MarkDownRenderer, so existing programs and prompts are unaffected.

Verification

  • Rendered both forms in a live Vite harness against openuiLibrary: string details (unchanged) and an example exercising all five union members inside one Steps
  • openuiLibrary.toSpec() signature updated as above; toJSONSchema() expands to anyOf: [{type: "string"}, {items: {anyOf: [$ref …]}}] with correct $defs refs
  • react-ui vitest: 6 passed; lang-core vitest: 76 passed
  • tsc --noEmit error count identical before/after the change (pre-existing react-headless d.ts drift only)

🤖 Generated with Claude Code

StepsItem's details prop was typed z.string() in the genui schema, so
LLMs could only express procedural steps as a single markdown string —
no CodeBlock, Image, or other content components, even though the
runtime and the React component layer (details: React.ReactNode)
already handle them.

Widen the schema to z.union([z.string(), z.array(StepsItemDetailsChildUnion)])
where the new union allows TextContent, MarkDownRenderer, CodeBlock,
Image, and ImageBlock. The union is intentionally narrower than
SectionContentChildUnion: procedural steps need prose, code, and
images, and referencing Steps from its own item schema would be
circular.

String details keep rendering through MarkDownRenderer, so existing
programs are unaffected; the renderer's existing renderNode branch
already renders element arrays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Shinyaigeek Shinyaigeek marked this pull request as ready for review July 14, 2026 04:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant