diff --git a/cockpit/chat/generative-ui/angular/src/app/app.config.ts b/cockpit/chat/generative-ui/angular/src/app/app.config.ts
index bbe3e5e49..1e72bb3c9 100644
--- a/cockpit/chat/generative-ui/angular/src/app/app.config.ts
+++ b/cockpit/chat/generative-ui/angular/src/app/app.config.ts
@@ -2,13 +2,11 @@
import { ApplicationConfig } from '@angular/core';
import { provideAgent } from '@cacheplane/angular';
import { provideChat } from '@cacheplane/chat';
-import { provideRender } from '@cacheplane/render';
import { environment } from '../environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideAgent({ apiUrl: environment.langGraphApiUrl }),
provideChat({}),
- provideRender({}),
],
};
diff --git a/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts
index 0089176c7..ebfd92f18 100644
--- a/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts
+++ b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts
@@ -1,42 +1,28 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component } from '@angular/core';
-import { ChatComponent, ChatGenerativeUiComponent } from '@cacheplane/chat';
+import { ChatComponent, views } from '@cacheplane/chat';
import { agent } from '@cacheplane/angular';
import { environment } from '../environments/environment';
+import { WeatherCardComponent } from './views/weather-card.component';
+import { StatCardComponent } from './views/stat-card.component';
+import { ContainerComponent } from './views/container.component';
+
+const myViews = views({
+ weather_card: WeatherCardComponent,
+ stat_card: StatCardComponent,
+ container: ContainerComponent,
+});
-/**
- * GenerativeUiComponent demonstrates dynamic UI generation within
- * chat messages using ChatComponent and ChatGenerativeUiComponent.
- * The agent embeds render specs that are rendered as live components.
- */
@Component({
selector: 'app-generative-ui',
standalone: true,
- imports: [ChatComponent, ChatGenerativeUiComponent],
- template: `
-
- `,
+ imports: [ChatComponent],
+ template: ``,
})
export class GenerativeUiComponent {
- protected readonly stream = agent({
+ protected readonly agentRef = agent({
apiUrl: environment.langGraphApiUrl,
- assistantId: environment.streamingAssistantId,
+ assistantId: environment.generativeUiAssistantId,
});
+ protected readonly myViews = myViews;
}
diff --git a/cockpit/chat/generative-ui/angular/src/app/views/container.component.ts b/cockpit/chat/generative-ui/angular/src/app/views/container.component.ts
new file mode 100644
index 000000000..29d5479a7
--- /dev/null
+++ b/cockpit/chat/generative-ui/angular/src/app/views/container.component.ts
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { Component, input } from '@angular/core';
+import type { Spec } from '@json-render/core';
+import { RenderElementComponent } from '@cacheplane/render';
+
+@Component({
+ selector: 'app-container',
+ standalone: true,
+ imports: [RenderElementComponent],
+ template: `
+
+ @for (key of childKeys(); track key) {
+
+ }
+
+ `,
+})
+export class ContainerComponent {
+ readonly childKeys = input([]);
+ readonly spec = input.required();
+}
diff --git a/cockpit/chat/generative-ui/angular/src/app/views/stat-card.component.ts b/cockpit/chat/generative-ui/angular/src/app/views/stat-card.component.ts
new file mode 100644
index 000000000..0d5407cb9
--- /dev/null
+++ b/cockpit/chat/generative-ui/angular/src/app/views/stat-card.component.ts
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-stat-card',
+ standalone: true,
+ template: `
+
+
{{ label() }}
+
{{ value() }}
+
+ `,
+})
+export class StatCardComponent {
+ readonly label = input('');
+ readonly value = input('');
+}
diff --git a/cockpit/chat/generative-ui/angular/src/app/views/weather-card.component.ts b/cockpit/chat/generative-ui/angular/src/app/views/weather-card.component.ts
new file mode 100644
index 000000000..9114640ec
--- /dev/null
+++ b/cockpit/chat/generative-ui/angular/src/app/views/weather-card.component.ts
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-weather-card',
+ standalone: true,
+ template: `
+
+
+
{{ city() }}
+ {{ weatherEmoji() }}
+
+
{{ temperature() }}°F
+
{{ condition() }}
+
+ `,
+})
+export class WeatherCardComponent {
+ readonly city = input('');
+ readonly temperature = input(0);
+ readonly condition = input('');
+
+ weatherEmoji(): string {
+ const c = this.condition().toLowerCase();
+ if (c.includes('sun') || c.includes('clear')) return '☀️';
+ if (c.includes('cloud') || c.includes('overcast')) return '☁️';
+ if (c.includes('rain')) return '🌧️';
+ if (c.includes('snow')) return '❄️';
+ if (c.includes('storm') || c.includes('thunder')) return '⛈️';
+ return '🌤️';
+ }
+}
diff --git a/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts b/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts
index 18c14185d..32ff7a3bb 100644
--- a/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts
+++ b/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts
@@ -1,5 +1,5 @@
export const environment = {
production: false,
- langGraphApiUrl: 'http://localhost:4508/api',
- streamingAssistantId: 'c-generative-ui',
+ langGraphApiUrl: 'http://localhost:4310/api',
+ generativeUiAssistantId: 'generative_ui',
};
diff --git a/cockpit/chat/generative-ui/angular/src/environments/environment.ts b/cockpit/chat/generative-ui/angular/src/environments/environment.ts
index 2c727e253..1cdf59a2a 100644
--- a/cockpit/chat/generative-ui/angular/src/environments/environment.ts
+++ b/cockpit/chat/generative-ui/angular/src/environments/environment.ts
@@ -1,5 +1,5 @@
export const environment = {
production: true,
langGraphApiUrl: '/api',
- streamingAssistantId: 'c-generative-ui',
+ generativeUiAssistantId: 'generative_ui',
};
diff --git a/cockpit/chat/generative-ui/python/docs/guide.md b/cockpit/chat/generative-ui/python/docs/guide.md
index 92f2ba78c..8a6c0ce60 100644
--- a/cockpit/chat/generative-ui/python/docs/guide.md
+++ b/cockpit/chat/generative-ui/python/docs/guide.md
@@ -1,76 +1,73 @@
-# Chat Generative UI with @cacheplane/chat
+# Generative UI with Streaming Auto-Detection
-Render dynamic UI components within chat messages using
-ChatGenerativeUiComponent. The agent embeds JSON render specs
-in responses that are rendered as live Angular components.
+Render dynamic UI components within chat messages using the streaming
+auto-detection pipeline. As tokens stream in, the system detects JSON,
+parses it incrementally, and renders Angular components in real time.
-Add generative UI to your chat interface using `ChatGenerativeUiComponent`
-from `@cacheplane/chat` and `provideRender()` from `@cacheplane/render`.
-Configure both providers to enable spec detection and rendering.
+Add generative UI to your chat interface using `views()` from
+`@cacheplane/chat`. Register view components and pass them to
+`ChatComponent` via the `[views]` input.
-
+
-Generative UI requires both `provideChat()` and `provideRender()`:
-
-```typescript
-import { provideRender } from '@cacheplane/render';
-import { provideChat } from '@cacheplane/chat';
-
-export const appConfig: ApplicationConfig = {
- providers: [
- provideStreamResource({ apiUrl: environment.langGraphApiUrl }),
- provideChat({}),
- provideRender({}),
- ],
-};
-```
+Create Angular components for each UI type the agent can emit.
+Each component uses `input()` signals to receive props from the
+rendered spec.
-
+
-Configure the backend agent to include JSON render specs in its
-responses using fenced code blocks with the `render-spec` tag.
+Use the `views()` function to map spec type names to Angular components:
-
-
+```typescript
+import { views } from '@cacheplane/chat';
-ChatGenerativeUiComponent automatically scans messages for render
-spec code blocks and extracts them for rendering.
+const myViews = views({
+ weather_card: WeatherCardComponent,
+ stat_card: StatCardComponent,
+ container: ContainerComponent,
+});
+```
-
+
-Use the component in your template alongside ChatComponent:
+Pass the view map to `ChatComponent` via the `[views]` input:
```html
-
-
+
```
-
+
-Register custom Angular components to handle specific spec types:
-
-```typescript
-provideRender({
- registry: defineAngularRegistry({
- card: MyCardComponent,
- chart: MyChartComponent,
- }),
-})
-```
+Instruct the LLM to respond with raw JSON following the Spec schema.
+No code fences or markdown — just valid JSON so the streaming pipeline
+can detect and parse it incrementally.
+## How Streaming Auto-Detection Works
+
+1. **Token streaming** — The LLM streams response tokens to the client.
+2. **ContentClassifier** — Inspects the incoming token buffer and detects
+ when the content is JSON rather than plain text or markdown.
+3. **Partial JSON parser** — As JSON tokens arrive, a partial parser
+ builds an incremental parse tree without waiting for the full payload.
+4. **ParseTreeStore** — Materializes the partial parse tree into a live
+ `Spec` object (elements map + root key) that updates on every chunk.
+5. **Component rendering** — The `[views]` registry resolves each element
+ type to an Angular component, which renders incrementally as the spec
+ grows.
+
-Generative UI bridges the gap between conversational AI and rich
-interactive interfaces — the agent can create forms, dashboards,
-and visualizations on the fly.
+Because detection and parsing happen on every streamed chunk, the user
+sees UI components materialize progressively — cards appear and fill in
+as the LLM generates the JSON structure.
diff --git a/cockpit/chat/generative-ui/python/langgraph.json b/cockpit/chat/generative-ui/python/langgraph.json
index 583df221b..cd12bd55e 100644
--- a/cockpit/chat/generative-ui/python/langgraph.json
+++ b/cockpit/chat/generative-ui/python/langgraph.json
@@ -1,6 +1,6 @@
{
"graphs": {
- "c-generative-ui": "./src/graph.py:graph"
+ "generative_ui": "./src/graph.py:graph"
},
"dependencies": ["."],
"python_version": "3.12",
diff --git a/cockpit/chat/generative-ui/python/prompts/generative-ui.md b/cockpit/chat/generative-ui/python/prompts/generative-ui.md
index a1991a980..98c06c985 100644
--- a/cockpit/chat/generative-ui/python/prompts/generative-ui.md
+++ b/cockpit/chat/generative-ui/python/prompts/generative-ui.md
@@ -1,22 +1,46 @@
-# Chat Generative UI Assistant
+# Generative UI Assistant
-You are an assistant that demonstrates dynamic UI generation within
-chat responses using render specs.
+You are a generative-UI assistant. You MUST respond with **raw JSON only** — no markdown, no code fences, no explanation text. Your entire response must be a single valid JSON object following the Spec format below.
-When the user asks you to create a UI element, include a JSON render spec
-in your response using a fenced code block with the `render-spec` language tag.
-For example:
+## Spec Schema
-```render-spec
+A **Spec** is a JSON object with two required top-level keys:
+
+```
+{
+ "elements": { [key: string]: Element },
+ "rootKey": string
+}
+```
+
+An **Element** has:
+
+```
{
- "type": "card",
- "props": { "title": "Generated Card" },
- "children": [
- { "type": "text", "props": { "content": "This card was generated by the AI" } }
- ]
+ "type": string, // component type name
+ "props": { ... }, // component-specific properties
+ "children?": string[] // ordered list of element keys (references into `elements`)
}
```
-The frontend will detect these specs and render them as live Angular
-components inline within the chat message. Explain what you are generating
-and why in the surrounding text.
+## Available Component Types
+
+| Type | Props | Children |
+|-----------------|--------------------------------------------------------------|----------|
+| `container` | *(none)* | Yes |
+| `weather_card` | `city` (string), `temperature` (number), `condition` (string)| No |
+| `stat_card` | `label` (string), `value` (string) | No |
+
+## Rules
+
+1. Respond ONLY with valid JSON. No markdown. No code fences. No surrounding text.
+2. Every element referenced in a `children` array must exist as a key in `elements`.
+3. `rootKey` must reference a key that exists in `elements`.
+4. Use `container` to group multiple cards together.
+5. Choose component types that best match the user's request.
+
+## Example Response
+
+If the user asks "What's the weather in Chicago and New York?", respond exactly like:
+
+{"elements":{"root":{"type":"container","props":{},"children":["chicago","nyc"]},"chicago":{"type":"weather_card","props":{"city":"Chicago","temperature":45,"condition":"Partly Cloudy"}},"nyc":{"type":"weather_card","props":{"city":"New York","temperature":52,"condition":"Sunny"}}},"rootKey":"root"}
diff --git a/docs/superpowers/plans/2026-04-08-generative-ui-spike.md b/docs/superpowers/plans/2026-04-08-generative-ui-spike.md
new file mode 100644
index 000000000..d05fde8ef
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-08-generative-ui-spike.md
@@ -0,0 +1,814 @@
+# Generative UI Spike — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a cockpit capability at `cockpit/langgraph/generative-ui/` that proves the streaming generative UI pipeline end-to-end — LLM streams JSON spec tokens → auto-detected → parsed → rendered as Angular components.
+
+**Architecture:** Follow the existing cockpit capability pattern (`cockpit/langgraph/streaming/` as template). Python graph instructs the LLM to return a JSON-render Spec. Angular frontend uses `ChatComponent` with `[views]` input — two simple view components (WeatherCard, StatCard). Registration via manifest + route-resolution.
+
+**Tech Stack:** LangGraph (Python), Angular 20+, `@cacheplane/chat`, `@cacheplane/render`, Tailwind CSS
+
+---
+
+## File Structure
+
+### New Files (Python Backend)
+
+| File | Purpose |
+|------|---------|
+| `cockpit/langgraph/generative-ui/python/src/graph.py` | LangGraph graph that generates JSON-render Specs |
+| `cockpit/langgraph/generative-ui/python/src/index.ts` | Module metadata |
+| `cockpit/langgraph/generative-ui/python/prompts/generative-ui.md` | System prompt with Spec schema |
+| `cockpit/langgraph/generative-ui/python/docs/guide.md` | Narrative docs |
+| `cockpit/langgraph/generative-ui/python/langgraph.json` | LangGraph config |
+| `cockpit/langgraph/generative-ui/python/project.json` | Nx project config |
+
+### New Files (Angular Frontend)
+
+| File | Purpose |
+|------|---------|
+| `cockpit/langgraph/generative-ui/angular/src/app/generative-ui.component.ts` | Main app component with ChatComponent + views |
+| `cockpit/langgraph/generative-ui/angular/src/app/app.config.ts` | Angular providers |
+| `cockpit/langgraph/generative-ui/angular/src/app/views/weather-card.component.ts` | WeatherCard view component |
+| `cockpit/langgraph/generative-ui/angular/src/app/views/stat-card.component.ts` | StatCard view component |
+| `cockpit/langgraph/generative-ui/angular/src/main.ts` | Bootstrap |
+| `cockpit/langgraph/generative-ui/angular/src/environments/environment.ts` | Production env |
+| `cockpit/langgraph/generative-ui/angular/src/environments/environment.development.ts` | Dev env |
+| `cockpit/langgraph/generative-ui/angular/src/index.html` | HTML shell |
+| `cockpit/langgraph/generative-ui/angular/src/styles.css` | Tailwind + design tokens |
+| `cockpit/langgraph/generative-ui/angular/project.json` | Nx Angular app config |
+| `cockpit/langgraph/generative-ui/angular/tsconfig.json` | TS config |
+| `cockpit/langgraph/generative-ui/angular/tsconfig.app.json` | App TS config |
+| `cockpit/langgraph/generative-ui/angular/proxy.conf.json` | Dev proxy |
+| `cockpit/langgraph/generative-ui/angular/package.json` | NPM metadata |
+| `cockpit/langgraph/generative-ui/angular/vercel.json` | Vercel build config |
+
+### Modified Files (Registration)
+
+| File | Change |
+|------|--------|
+| `libs/cockpit-registry/src/lib/manifest.ts` | Add `'generative-ui'` to langgraph core-capabilities |
+| `apps/cockpit/src/lib/route-resolution.ts` | Import and register the module |
+
+---
+
+### Task 1: Python Backend — Graph + Prompt + Config
+
+**Files:**
+- Create: `cockpit/langgraph/generative-ui/python/src/graph.py`
+- Create: `cockpit/langgraph/generative-ui/python/prompts/generative-ui.md`
+- Create: `cockpit/langgraph/generative-ui/python/langgraph.json`
+- Create: `cockpit/langgraph/generative-ui/python/src/index.ts`
+- Create: `cockpit/langgraph/generative-ui/python/project.json`
+- Create: `cockpit/langgraph/generative-ui/python/docs/guide.md`
+
+- [ ] **Step 1: Create the system prompt**
+
+Create `cockpit/langgraph/generative-ui/python/prompts/generative-ui.md`:
+
+````markdown
+# Generative UI Assistant
+
+You are a helpful assistant that responds with structured JSON UI specifications.
+
+When the user asks about weather, locations, or data, respond with a JSON object that follows this exact schema:
+
+```json
+{
+ "root": "",
+ "elements": {
+ "": {
+ "type": "",
+ "props": { ... },
+ "children": ["", ""]
+ }
+ }
+}
+```
+
+## Available component types
+
+### `container`
+A layout wrapper that renders its children vertically.
+- Props: none required
+- Children: array of element keys
+
+### `weather_card`
+Displays weather information for a city.
+- Props:
+ - `city` (string): City name
+ - `temperature` (number): Temperature in Fahrenheit
+ - `condition` (string): Weather condition (e.g., "Sunny", "Cloudy", "Rainy")
+
+### `stat_card`
+Displays a single statistic.
+- Props:
+ - `label` (string): What the stat measures (e.g., "Humidity", "Wind Speed")
+ - `value` (string): The formatted value (e.g., "65%", "12 mph")
+
+## Rules
+
+1. Always respond with ONLY a valid JSON object — no markdown, no explanation, no code fences
+2. Use a `container` as the root element when you have multiple components
+3. Give each element a unique key (e.g., "root", "weather", "stat-1", "stat-2")
+4. Include 2-4 elements total for variety
+5. Make the data realistic and varied
+
+## Example response
+
+For "What's the weather in Seattle?":
+
+```json
+{
+ "root": "root",
+ "elements": {
+ "root": {
+ "type": "container",
+ "props": {},
+ "children": ["weather", "stat-humidity", "stat-wind"]
+ },
+ "weather": {
+ "type": "weather_card",
+ "props": {
+ "city": "Seattle",
+ "temperature": 58,
+ "condition": "Overcast"
+ }
+ },
+ "stat-humidity": {
+ "type": "stat_card",
+ "props": {
+ "label": "Humidity",
+ "value": "78%"
+ }
+ },
+ "stat-wind": {
+ "type": "stat_card",
+ "props": {
+ "label": "Wind Speed",
+ "value": "8 mph NW"
+ }
+ }
+ }
+}
+```
+````
+
+- [ ] **Step 2: Create the Python graph**
+
+Create `cockpit/langgraph/generative-ui/python/src/graph.py`:
+
+```python
+"""
+LangGraph Generative UI Graph
+
+A StateGraph that instructs the LLM to return JSON-render Spec objects.
+The Angular frontend auto-detects the JSON and renders it as Angular
+components via the streaming generative UI pipeline.
+"""
+
+from pathlib import Path
+from langgraph.graph import StateGraph, MessagesState, END
+from langchain_openai import ChatOpenAI
+from langchain_core.messages import SystemMessage
+
+PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
+
+
+def build_generative_ui_graph():
+ llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
+
+ async def generate(state: MessagesState) -> dict:
+ system_prompt = (PROMPTS_DIR / "generative-ui.md").read_text()
+ messages = [SystemMessage(content=system_prompt)] + state["messages"]
+ response = await llm.ainvoke(messages)
+ return {"messages": [response]}
+
+ graph = StateGraph(MessagesState)
+ graph.add_node("generate", generate)
+ graph.set_entry_point("generate")
+ graph.add_edge("generate", END)
+
+ return graph.compile()
+
+
+graph = build_generative_ui_graph()
+```
+
+- [ ] **Step 3: Create langgraph.json**
+
+Create `cockpit/langgraph/generative-ui/python/langgraph.json`:
+
+```json
+{
+ "graphs": {
+ "generative_ui": "./src/graph.py:graph"
+ },
+ "dependencies": ["."],
+ "python_version": "3.12",
+ "env": ".env"
+}
+```
+
+- [ ] **Step 4: Create module metadata**
+
+Create `cockpit/langgraph/generative-ui/python/src/index.ts`:
+
+```typescript
+export interface CockpitCapabilityModule {
+ id: string;
+ manifestIdentity: {
+ product: 'langgraph';
+ section: 'core-capabilities';
+ topic: 'generative-ui';
+ page: 'overview';
+ language: 'python';
+ };
+ title: string;
+ docsPath: string;
+ promptAssetPaths: string[];
+ codeAssetPaths: string[];
+ backendAssetPaths: string[];
+ docsAssetPaths: string[];
+ runtimeUrl?: string;
+ devPort?: number;
+}
+
+export const langgraphGenerativeUiPythonModule: CockpitCapabilityModule = {
+ id: 'langgraph-generative-ui-python',
+ manifestIdentity: {
+ product: 'langgraph',
+ section: 'core-capabilities',
+ topic: 'generative-ui',
+ page: 'overview',
+ language: 'python',
+ },
+ title: 'LangGraph Generative UI (Python)',
+ docsPath: '/docs/langgraph/core-capabilities/generative-ui/overview/python',
+ promptAssetPaths: ['cockpit/langgraph/generative-ui/python/prompts/generative-ui.md'],
+ codeAssetPaths: [
+ 'cockpit/langgraph/generative-ui/angular/src/app/generative-ui.component.ts',
+ 'cockpit/langgraph/generative-ui/angular/src/app/app.config.ts',
+ 'cockpit/langgraph/generative-ui/angular/src/app/views/weather-card.component.ts',
+ 'cockpit/langgraph/generative-ui/angular/src/app/views/stat-card.component.ts',
+ ],
+ backendAssetPaths: [
+ 'cockpit/langgraph/generative-ui/python/src/graph.py',
+ ],
+ docsAssetPaths: ['cockpit/langgraph/generative-ui/python/docs/guide.md'],
+ runtimeUrl: 'langgraph/generative-ui',
+ devPort: 4310,
+};
+```
+
+- [ ] **Step 5: Create Nx project config**
+
+Create `cockpit/langgraph/generative-ui/python/project.json`:
+
+```json
+{
+ "name": "cockpit-langgraph-generative-ui-python",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "cockpit/langgraph/generative-ui/python/src",
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "@nx/js:tsc",
+ "outputs": ["{workspaceRoot}/dist/cockpit/langgraph/generative-ui/python"],
+ "options": {
+ "outputPath": "dist/cockpit/langgraph/generative-ui/python",
+ "main": "cockpit/langgraph/generative-ui/python/src/index.ts",
+ "tsConfig": "cockpit/langgraph/generative-ui/python/tsconfig.json"
+ }
+ },
+ "smoke": {
+ "executor": "nx:run-commands",
+ "options": {
+ "cwd": "cockpit/langgraph/generative-ui/python",
+ "command": "npx tsx -e \"import { langgraphGenerativeUiPythonModule } from './src/index.ts'; const m = langgraphGenerativeUiPythonModule; if (m.id !== 'langgraph-generative-ui-python') { throw new Error('Unexpected module: ' + m.id); }\""
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 6: Create docs guide**
+
+Create `cockpit/langgraph/generative-ui/python/docs/guide.md`:
+
+````markdown
+# Generative UI
+
+This example demonstrates streaming generative UI — an LLM returns JSON-render Specs that are auto-detected and rendered as Angular components in real time.
+
+## How It Works
+
+1. The LangGraph agent receives a user message
+2. The LLM generates a JSON-render Spec as its response (not markdown)
+3. Tokens stream to the Angular frontend via SSE
+4. `ChatComponent` auto-detects the JSON via `ContentClassifier`
+5. `@cacheplane/partial-json` parses the incomplete JSON character-by-character
+6. `ParseTreeStore` materializes the parse tree into a `Spec` signal
+7. `RenderSpecComponent` renders the spec using the view registry
+8. Components update live as tokens arrive — string props grow visibly
+
+## View Components
+
+This example registers two view components:
+
+- **WeatherCard** — Displays city, temperature, and weather condition
+- **StatCard** — Displays a label/value pair (humidity, wind speed, etc.)
+
+## Key Code
+
+```typescript
+// Register views
+const myViews = views({
+ weather_card: WeatherCardComponent,
+ stat_card: StatCardComponent,
+ container: ContainerComponent,
+});
+
+// Pass to ChatComponent
+
+```
+
+No manual JSON parsing, no content type detection, no spec wiring — the `ChatComponent` handles everything automatically.
+````
+
+- [ ] **Step 7: Create Python tsconfig.json**
+
+Create `cockpit/langgraph/generative-ui/python/tsconfig.json`:
+
+```json
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc"
+ },
+ "include": ["src/**/*.ts"]
+}
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add cockpit/langgraph/generative-ui/python/
+git commit -m "feat(cockpit): add generative UI Python graph and module metadata"
+```
+
+---
+
+### Task 2: Angular Frontend — View Components + App
+
+**Files:**
+- Create: All files under `cockpit/langgraph/generative-ui/angular/`
+
+- [ ] **Step 1: Create WeatherCardComponent**
+
+Create `cockpit/langgraph/generative-ui/angular/src/app/views/weather-card.component.ts`:
+
+```typescript
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-weather-card',
+ standalone: true,
+ template: `
+
+
+
{{ city() }}
+ {{ weatherEmoji() }}
+
+
{{ temperature() }}°F
+
{{ condition() }}
+
+ `,
+})
+export class WeatherCardComponent {
+ readonly city = input('');
+ readonly temperature = input(0);
+ readonly condition = input('');
+
+ weatherEmoji(): string {
+ const c = this.condition().toLowerCase();
+ if (c.includes('sun') || c.includes('clear')) return '☀️';
+ if (c.includes('cloud') || c.includes('overcast')) return '☁️';
+ if (c.includes('rain')) return '🌧️';
+ if (c.includes('snow')) return '❄️';
+ if (c.includes('storm') || c.includes('thunder')) return '⛈️';
+ return '🌤️';
+ }
+}
+```
+
+- [ ] **Step 2: Create StatCardComponent**
+
+Create `cockpit/langgraph/generative-ui/angular/src/app/views/stat-card.component.ts`:
+
+```typescript
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-stat-card',
+ standalone: true,
+ template: `
+
+
{{ label() }}
+
{{ value() }}
+
+ `,
+})
+export class StatCardComponent {
+ readonly label = input('');
+ readonly value = input('');
+}
+```
+
+- [ ] **Step 3: Create ContainerComponent**
+
+Create `cockpit/langgraph/generative-ui/angular/src/app/views/container.component.ts`:
+
+```typescript
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { Component, input } from '@angular/core';
+import type { Spec } from '@json-render/core';
+import { RenderElementComponent } from '@cacheplane/render';
+
+@Component({
+ selector: 'app-container',
+ standalone: true,
+ imports: [RenderElementComponent],
+ template: `
+
+ @for (key of childKeys(); track key) {
+
+ }
+
+ `,
+})
+export class ContainerComponent {
+ readonly childKeys = input([]);
+ readonly spec = input.required();
+}
+```
+
+- [ ] **Step 4: Create main app component**
+
+Create `cockpit/langgraph/generative-ui/angular/src/app/generative-ui.component.ts`:
+
+```typescript
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { Component } from '@angular/core';
+import { ChatComponent, views } from '@cacheplane/chat';
+import { agent } from '@cacheplane/angular';
+import { environment } from '../environments/environment';
+import { WeatherCardComponent } from './views/weather-card.component';
+import { StatCardComponent } from './views/stat-card.component';
+import { ContainerComponent } from './views/container.component';
+
+const myViews = views({
+ weather_card: WeatherCardComponent,
+ stat_card: StatCardComponent,
+ container: ContainerComponent,
+});
+
+@Component({
+ selector: 'app-generative-ui',
+ standalone: true,
+ imports: [ChatComponent],
+ template: ``,
+})
+export class GenerativeUiComponent {
+ protected readonly agentRef = agent({
+ apiUrl: environment.langGraphApiUrl,
+ assistantId: environment.generativeUiAssistantId,
+ });
+
+ protected readonly myViews = myViews;
+}
+```
+
+- [ ] **Step 5: Create app config**
+
+Create `cockpit/langgraph/generative-ui/angular/src/app/app.config.ts`:
+
+```typescript
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { ApplicationConfig } from '@angular/core';
+import { provideAgent } from '@cacheplane/angular';
+import { provideChat } from '@cacheplane/chat';
+import { environment } from '../environments/environment';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideAgent({ apiUrl: environment.langGraphApiUrl }),
+ provideChat({}),
+ ],
+};
+```
+
+- [ ] **Step 6: Create bootstrap + environments + config files**
+
+Create `cockpit/langgraph/generative-ui/angular/src/main.ts`:
+
+```typescript
+// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { GenerativeUiComponent } from './app/generative-ui.component';
+
+bootstrapApplication(GenerativeUiComponent, appConfig).catch(console.error);
+```
+
+Create `cockpit/langgraph/generative-ui/angular/src/environments/environment.ts`:
+
+```typescript
+export const environment = {
+ production: true,
+ langGraphApiUrl: '/api',
+ generativeUiAssistantId: 'generative_ui',
+};
+```
+
+Create `cockpit/langgraph/generative-ui/angular/src/environments/environment.development.ts`:
+
+```typescript
+export const environment = {
+ production: false,
+ langGraphApiUrl: 'http://localhost:4310/api',
+ generativeUiAssistantId: 'generative_ui',
+};
+```
+
+Create `cockpit/langgraph/generative-ui/angular/src/index.html`:
+
+```html
+
+
+
+
+ LangGraph Generative UI — Angular
+
+
+
+
+
+
+
+
+```
+
+Create `cockpit/langgraph/generative-ui/angular/src/styles.css` (copy streaming pattern):
+
+```css
+@import "../../../../../libs/design-tokens/src/lib/tokens.css";
+@import "tailwindcss";
+@source "../../../../../libs/chat/src/";
+
+@theme {
+ --color-bg: var(--ds-bg);
+ --color-surface: #ffffff;
+ --color-accent: var(--ds-accent);
+ --color-accent-light: var(--ds-accent-light);
+ --color-text-primary: var(--ds-text-primary);
+ --color-text-secondary: var(--ds-text-secondary);
+ --color-text-muted: var(--ds-text-muted);
+ --color-border: var(--ds-accent-border);
+ --color-error: #ef4444;
+ --color-success: #22c55e;
+ --font-sans: var(--ds-font-sans);
+ --font-serif: var(--ds-font-serif);
+ --font-mono: var(--ds-font-mono);
+}
+
+*, *::before, *::after { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ font-family: var(--ds-font-sans);
+ background: var(--ds-bg);
+ color: var(--ds-text-primary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+```
+
+Create `cockpit/langgraph/generative-ui/angular/proxy.conf.json`:
+
+```json
+{
+ "/api": {
+ "target": "http://localhost:8123",
+ "secure": false,
+ "changeOrigin": true,
+ "pathRewrite": { "^/api": "" },
+ "ws": true
+ }
+}
+```
+
+Create `cockpit/langgraph/generative-ui/angular/package.json`:
+
+```json
+{
+ "name": "@cacheplane/cockpit-langgraph-generative-ui-angular",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@cacheplane/chat": "^0.0.1",
+ "@cacheplane/render": "^0.0.1",
+ "@cacheplane/angular": "^0.0.1",
+ "@json-render/core": "^0.16.0",
+ "@langchain/langgraph-sdk": "^0.0.36"
+ },
+ "license": "PolyForm-Noncommercial-1.0.0",
+ "sideEffects": false
+}
+```
+
+Create `cockpit/langgraph/generative-ui/angular/vercel.json`:
+
+```json
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "buildCommand": "npx nx build cockpit-langgraph-generative-ui-angular",
+ "outputDirectory": "dist/cockpit/langgraph/generative-ui/angular/browser",
+ "framework": null
+}
+```
+
+Create `cockpit/langgraph/generative-ui/angular/tsconfig.json`:
+
+```json
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "noPropertyAccessFromIndexSignature": false,
+ "experimentalDecorators": true,
+ "module": "preserve",
+ "emitDeclarationOnly": false,
+ "composite": false,
+ "lib": ["es2022", "dom"],
+ "skipLibCheck": true,
+ "strict": false
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": false,
+ "strictInputAccessModifiers": false,
+ "strictTemplates": false
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ { "path": "./tsconfig.app.json" }
+ ]
+}
+```
+
+Create `cockpit/langgraph/generative-ui/angular/tsconfig.app.json`:
+
+```json
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "lib": ["es2022", "dom"],
+ "types": [],
+ "emitDeclarationOnly": false
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.ts"]
+}
+```
+
+Create `cockpit/langgraph/generative-ui/angular/project.json`:
+
+```json
+{
+ "name": "cockpit-langgraph-generative-ui-angular",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "cockpit/langgraph/generative-ui/angular/src",
+ "projectType": "application",
+ "targets": {
+ "build": {
+ "executor": "@angular/build:application",
+ "outputs": ["{options.outputPath.base}"],
+ "options": {
+ "outputPath": {
+ "base": "dist/cockpit/langgraph/generative-ui/angular",
+ "browser": ""
+ },
+ "browser": "cockpit/langgraph/generative-ui/angular/src/main.ts",
+ "tsConfig": "cockpit/langgraph/generative-ui/angular/tsconfig.app.json",
+ "styles": ["cockpit/langgraph/generative-ui/angular/src/styles.css"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" },
+ { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" }
+ ],
+ "outputHashing": "none"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "fileReplacements": [
+ {
+ "replace": "cockpit/langgraph/generative-ui/angular/src/environments/environment.ts",
+ "with": "cockpit/langgraph/generative-ui/angular/src/environments/environment.development.ts"
+ }
+ ]
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "continuous": true,
+ "executor": "@angular/build:dev-server",
+ "configurations": {
+ "production": { "buildTarget": "cockpit-langgraph-generative-ui-angular:build:production" },
+ "development": { "buildTarget": "cockpit-langgraph-generative-ui-angular:build:development" }
+ },
+ "defaultConfiguration": "development",
+ "options": {
+ "proxyConfig": "cockpit/langgraph/generative-ui/angular/proxy.conf.json"
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add cockpit/langgraph/generative-ui/angular/
+git commit -m "feat(cockpit): add generative UI Angular frontend with view components"
+```
+
+---
+
+### Task 3: Registration — Manifest + Route Resolution
+
+**Files:**
+- Modify: `libs/cockpit-registry/src/lib/manifest.ts`
+- Modify: `apps/cockpit/src/lib/route-resolution.ts`
+
+- [ ] **Step 1: Add generative-ui to APPROVED_TOPICS**
+
+In `libs/cockpit-registry/src/lib/manifest.ts`, add `'generative-ui'` to the langgraph core-capabilities array:
+
+```typescript
+langgraph: {
+ 'getting-started': ['overview'],
+ 'core-capabilities': [
+ 'persistence',
+ 'durable-execution',
+ 'streaming',
+ 'interrupts',
+ 'memory',
+ 'subgraphs',
+ 'time-travel',
+ 'deployment-runtime',
+ 'generative-ui',
+ ],
+},
+```
+
+- [ ] **Step 2: Register module in route-resolution.ts**
+
+Add import at the top:
+
+```typescript
+import { langgraphGenerativeUiPythonModule } from '../../../../cockpit/langgraph/generative-ui/python/src/index';
+```
+
+Add to the `capabilityModules` array:
+
+```typescript
+const capabilityModules = [
+ // ... existing modules ...
+ langgraphGenerativeUiPythonModule,
+];
+```
+
+- [ ] **Step 3: Verify smoke test passes**
+
+Run: `export PATH="/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH" && npx nx smoke cockpit-langgraph-generative-ui-python`
+Expected: PASS
+
+- [ ] **Step 4: Verify Angular build succeeds**
+
+Run: `export PATH="/Users/blove/.nvm/versions/node/v22.14.0/bin:$PATH" && npx nx build cockpit-langgraph-generative-ui-angular`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add libs/cockpit-registry/ apps/cockpit/
+git commit -m "feat(cockpit): register generative-ui capability in manifest and routes"
+```
diff --git a/docs/superpowers/specs/2026-04-08-generative-ui-spike-design.md b/docs/superpowers/specs/2026-04-08-generative-ui-spike-design.md
new file mode 100644
index 000000000..309426dab
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-08-generative-ui-spike-design.md
@@ -0,0 +1,53 @@
+# Generative UI Spike — Design Spec
+
+**Date:** 2026-04-08
+**Status:** Draft
+
+## Goal
+
+Prove the streaming generative UI pipeline end-to-end: LangGraph agent returns a JSON-render Spec as AI message content → tokens stream → `ContentClassifier` auto-detects JSON → `PartialJsonParser` builds tree → `ParseTreeStore` materializes Spec → `` renders Angular components that update live as tokens arrive.
+
+## Approach
+
+Add a new cockpit capability under `cockpit/langgraph/generative-ui/` following the existing capability pattern. The agent returns a full JSON-render Spec as its message content (not as a tool call result). The Angular frontend uses `ChatComponent` with `[views]` input — relying entirely on auto-detection.
+
+## Python Graph
+
+A single-node LangGraph graph (`MessagesState`). The node calls an LLM with a system prompt that instructs it to respond with a JSON-render Spec containing 2-3 elements: a `WeatherCard` and one or two `StatCard` components. The LLM streams the JSON token-by-token.
+
+The system prompt includes the exact Spec schema and available component types so the LLM knows what to generate.
+
+## Angular Frontend
+
+### View Components
+
+**WeatherCard** — Displays city name, temperature, condition, and an icon. Inputs: `city: string`, `temperature: number`, `condition: string`.
+
+**StatCard** — Displays a label and value (e.g., "Humidity: 65%"). Inputs: `label: string`, `value: string`.
+
+Both are simple standalone components with Tailwind styling.
+
+### App Component
+
+Uses `ChatComponent` with `[views]` input passing a `ViewRegistry` mapping `weather_card` → `WeatherCardComponent` and `stat_card` → `StatCardComponent`.
+
+## Registration
+
+- Add `generative-ui` topic to the `langgraph` product's `core-capabilities` section in the cockpit manifest
+- Create module metadata (`CockpitCapabilityModule`) in the Python `src/index.ts`
+- Register in `route-resolution.ts`'s `capabilityModules` array
+
+## What It Proves
+
+1. The parse tree correctly builds from character-by-character LLM output
+2. Materialization produces valid `Spec` objects that `` can render
+3. Structural sharing works — unchanged elements keep references, render lib skips re-render
+4. Character-level prop streaming is visible in the rendered UI (e.g., city name filling in letter by letter)
+5. The full pipeline works without any manual wiring — just `[views]` input on `ChatComponent`
+
+## What It Defers
+
+- Real-world data (Phase 2: analytics dashboard with charts, data grids, tool calling)
+- A2UI support
+- Production error handling for malformed JSON
+- Interactive state store integration