The agent system prompt instructed the LLM to return <ui>...</ui> XML markup,
but the frontend renderer uses useJsonParser(message.content, kit.schema)
which expects a JSON object matching createUiKit(...).schema:
{
"ui": [
{ "metric": { "props": { "label": "...", "value": "..." } } },
{ "pieChart": { "props": { "title": "...", "data": "[{...}]" } } },
...
]
}
The XML syntax in useUiKit({ examples }) is hashbrowns prompt DSL for when
hashbrown drives the LLM directly (e.g. useUiChat); because this demo drives
the LLM via langgraph, the agent itself must emit the JSON wire format. With
XML content, useJsonParser returned { value: undefined } and the renderer
bailed to null, leaving the chat with only a loading indicator.
Rewrote the system prompt to produce the JSON envelope and updated the
renderer top-of-file docstring to match. Prop schemas tracked from the
current kit (metric: label+value, pieChart/barChart: title+data-as-JSON-string,
dealCard: title+stage+value, Markdown: children).
The multimodal demo failed three ways:
1. Try-with-sample-image rendered a broken thumbnail because the sample
PNG on disk is a Git LFS pointer stub when LFS is not pulled at build
time. The browser base64-encoded the 130-byte text stub, fed it into
the chat as a valid-looking image/png, and CopilotChat rendered it
as a broken <img>.
2. Try-with-sample-pdf attached in the composer but the agent said
"no image/document attached". Same broken-image pipeline also hid the
real symptom: the published @ag-ui/langgraph converter (0.0.x) only
understands the legacy { type: binary, mimeType, data | url }
AG-UI content-part shape. The modern { type: image | document,
source: {...} } parts CopilotChat emits are silently filtered out of
the LangChain message stream, so the LangGraph agent never saw them.
3. Manual drag-and-drop of an image failed for the same reason as #2.
Fixes:
- sample-attachment-buttons.tsx: reject Git LFS pointer stubs and
validate PNG/PDF magic bytes on the client before feeding them to the
attachment queue. Surfaces an actionable error pointing at
git-lfs-pull instead of silently producing a broken thumbnail.
- page.tsx: install an onRunInitialized subscriber on the active agent
that rewrites outgoing user-message image / document / audio / video
parts to the legacy binary shape the runtime converter preserves.
Everything else (CopilotChat UI, useAttachments upload pipeline,
paperclip + drag-drop + paste paths) is untouched; we only retarget
the wire format so the LangGraph side receives the attachment.
- multimodal_agent.py: after the rewrite, attachments arrive in the
agent as LangChain image_url content parts (data URLs) regardless of
upstream modality. Route on MIME instead of the AG-UI part type:
image/* forwards to GPT-4o natively, application/pdf is flattened to
text via pypdf. The modern document shape is still handled for
forward-compat when the runtime is upgraded.
Verified: classifier helpers pass inline unit checks; tsc --noEmit
introduces no new type errors relative to HEAD (module-resolution
errors for @copilotkit/shared and lucide-react are pre-existing on this
branch and unrelated to the multimodal demo).
## Summary
On integration profile pages (e.g. `/integrations/langgraph-python`),
the `cli-start` demo entry — which has a `command` field instead of a
`route` — was being rendered inside the Live Demos grid. Clicking it
opened `DemoDrawer` with an undefined `demoRoute`, so the iframe tried
to load `${backend_url}undefined`.
This PR:
- Splits `integration.demos` into `liveDemos` (runnable, have `route`)
and `commandDemos` (CLI-only, have `command`).
- Filters command-only entries out of the Live Demos grid.
- Adds a new **Get Started** section above Live Demos that renders each
`commandDemos` entry as a description + copy-able `<code>` block
(mirrors how `shell-dashboard` already handles these via `CommandCell`).
- Makes `Demo.route` optional and adds `Demo.command?: string` on the
`Demo` type.
Only `langgraph-python` currently has a command-only demo, so this is
the only profile page that changes visibly.
## Test plan
- [x] Ran `showcase/shell` dev server, visited
`/integrations/langgraph-python`, confirmed:
- New "Get Started" card shows `npx copilotkit@latest init --framework
langgraph-python` with a working Copy button.
- "Live Demos" grid no longer contains the broken "CLI Start Command"
tile.
- Other integrations (no `command` demos) render unchanged — Live Demos
still present, no empty "Get Started" section.
- [x] Pre-commit hooks (lint-fix, check-binaries,
test-and-check-packages — 1149 tests, commitlint) all pass.
Starting the /demos/auth cell unauthenticated meant the initial `/info`
handshake from CopilotKit returned 401 before any React error handler
could attach, crashing the page on load. Flip the default so the demo
mounts already signed in — the chat is immediately usable — and expose
Sign out / Sign in as the toggle for exercising the 401 path.
- use-demo-auth: default state flipped from unauthed to authed so the
page mounts with a valid Bearer header.
- page.tsx: add a ChatErrorBoundary around <CopilotChat /> that catches
any render-time throw from chat internals after signing out and shows
a friendly in-page message instead of white-screening. The boundary
resets on auth transitions so signing back in restores a live chat.
- auth-banner: rename Authenticate -> Sign in; status copy updated to
Signed in / Signed out to match the inverted default.
- tests/e2e/auth.spec.ts: flip assertions — page loads authenticated,
sign out triggers 401 on next send with banner still mounted, and
sign in clears the error and restores successful sends.
- qa/auth.md: refreshed checklist for the authenticated-default flow.
V1 CopilotRuntime wrapper silently drops `transcriptionService`, so the
composer never saw the mic button on /voice. Write it onto the V2 runtime
instance directly, and wrap the OpenAI service with a guard that throws an
"api key missing" error when OPENAI_API_KEY is absent (mapped by the
runtime error categorizer to AUTH_FAILED / 401) instead of the opaque 503
users were hitting.
Clicking any suggestion in the byoc-json-render demo crashed with
"useVisibility must be used within a VisibilityProvider". Renderer from
@json-render/react relies on StateProvider, VisibilityProvider,
ValidationProvider, and ActionProvider contexts that its internal
ElementRenderer consumes, but the assistant-message slot was rendering
Renderer bare.
Wrap it in JSONUIProvider (which composes all four required providers)
so streamed spec output renders without a context crash.
Also pass children through the MetricCard catalog wrapper. The agent's
Sales Dashboard worked example uses a MetricCard as the root with a
BarChart nested in its children array; previously that chart was
silently dropped by the registry wrapper, which only rendered the
MetricCard itself.
Wrap PieChart + BarChart with string-accepting wrappers that JSON-parse the
data prop, and change the hashbrown schema from s.streaming.array(...) to
s.string(). Drop the non-schema example props (trend on metric, assignee
+ dueDate on dealCard).
Wave 4a was ported from the starter, which used an older hashbrown where:
- optional props were declared via .optional()
- array props accepted stringified JSON in example JSX attributes
@hashbrownai/core@0.5.0-beta.4 dropped .optional() (props omitted from the
schema are treated as not-present) AND tightened example-prompt validation
to reject strings against array schemas. The build failed at prerender:
Error: Example prompt has 5 errors:
Prop 'trend' is not defined on <metric>
Prop 'data' on <pieChart>: Expected an array at: <root>
Prop 'data' on <barChart>: Expected an array at: <root>
Prop 'assignee' is not defined on <dealCard>
Prop 'dueDate' is not defined on <dealCard>
Modeling data as a string matches how the LLM streams it anyway (JSON
tokens through useJsonParser), and the wrappers are defensive about
partial tokens — they render nothing if the parse fails mid-stream.
Review feedback from #4196:
- `[slug]/[demo]/page.tsx` constructed `${backend_url}${demo.route}`
without a null check, so command-only demos (which have no `route`)
rendered an iframe pointing at `${backend_url}undefined`. Now builds
the src only when `demo.route` exists and renders a 'no live preview'
panel otherwise, mirroring the Get Started section on the profile
page. Also replaces the `any`-typed state with proper `Demo` and
`Integration` types imported from `@/lib/registry`.
- `[slug]/[demo]/preview/page.tsx` had the same bug — already typed
but TypeScript doesn't catch template-literal coercion of undefined.
Now bails with a command-focused message before concatenating.
- `profile-client.tsx` no longer duplicates `Demo`/`Integration`
interfaces — deleted the local copies and imports from
`@/lib/registry`. copyDemoCommand's catch now logs the failure so a
double-failure (no clipboard API + blocked prompt) is diagnosable.
Comment above the live-demos section updated from 'Demos' to
'Live Demos' to match the rendered heading.
The voice route was instantiating OpenAI() at module scope, which Next.js
triggers during 'collect page data' at build time. The Docker build context
does NOT have OPENAI_API_KEY set — only the running container does — so
every showcase-langgraph-python deploy has been failing since the voice
demo merged (#4224):
Error: Missing credentials. Please pass an `apiKey`, or set the
`OPENAI_API_KEY` environment variable.
[Error: Failed to collect page data for /api/copilotkit-voice]
Wrap the OpenAI + TranscriptionService + CopilotRuntime construction in
a getRuntime() function that runs on first request. Cached after first
call so there's no per-request cost in production.
This unblocks the full showcase deploy pipeline, not just voice.
Add *.wav to .gitattributes so audio assets follow the same LFS policy
as the existing *.png / *.pdf / *.gif / *.jpg / *.jpeg / *.mp4 / *.webm
rules. Re-stage sample.wav so the committed object becomes an LFS
pointer instead of a raw 87 KB blob.
- public/demo-audio/sample.wav (87KB, 16kHz mono, ~2.7s) — TTS "What is the weather in Tokyo?" for the voice demo's "Play sample" button
- public/demo-files/sample.png (10KB) — CopilotKit logo for the multimodal demo's "Try with sample image"
- public/demo-files/sample.pdf (2.4KB, one page, contains "CopilotKit" 9x) — quickstart excerpt for the multimodal demo's "Try with sample PDF"
Makes both demos fully self-contained end-to-end; the "Try with sample X" buttons now work without the user providing their own files.
Re-run `generate-starters.ts` for langgraph-python to emit the
byoc_json_render_agent.py starter agent and register it in langgraph.json
(post-merge drift-check was failing because the starter hadn't been
regenerated after adding the byoc-json-render demo).
Bump generate-catalog.test LGP-feature-count assertions 32->33 and
wired-totals 176->177 / 31->32, unshipped 486->485 / 6->5 to match the
new manifest.
Regenerate langgraph-python starter so agent_config_agent is included
(drift-check fails without this). Bump generate-catalog.test.ts counts
to match wiring of agent-config: LGP wired 31→32, LGP unshipped 6→5,
total wired 176→177, total unshipped 486→485.
Resolves conflicts:
- manifest.yaml / constraints.yaml / docs-links.json: keep both byoc-json-render
and main's re-wired open-gen-ui + open-gen-ui-advanced entries
- scripts/hooks/check-binaries.sh: keep both shell-docs and shell-dojo
demo-content exclusions (identical lines, different ordering)
- pnpm-lock.yaml (langgraph-python): regenerated via pnpm install
- shell/shell-dojo/shell-docs data JSONs: accept main's deletion (now
regenerated by nx targets), then regenerate via scripts/generate-registry.ts
and scripts/bundle-demo-content.ts
Bump generate-registry.test expected feature/demo count 32 -> 33 to account
for the new byoc-json-render demo.
Resolves textual conflicts in manifest.yaml and docs-links.json (keeps
both open-gen-ui* from main and agent-config from this branch). Accepts
main's deletion of generated shell*/src/data/*.json (now gitignored).
Bumps langgraph-python feature/demo count test from 32 to 33.
Resolved conflicts:
- showcase/packages/langgraph-python/manifest.yaml: kept auth (HEAD) +
open-gen-ui / open-gen-ui-advanced (main) in both features and demos.
- showcase/packages/langgraph-python/docs-links.json: kept auth entry
from HEAD alongside updated subagents shell_docs_path.
- showcase/shared/constraints.yaml: kept auth + open-gen-ui* entries.
- Deleted shell/shell-dojo/shell-docs generated JSONs per main (now
generated at build, no longer committed).
Bumped langgraph-python feature/demo count 32 -> 33 in generate-registry
test to reflect the added auth demo.
## Summary
Widens the e2e-demos probe from LangGraph Python only (Wave 1) to all 17
integrations. Every integration now gets per-demo D3 structural checks —
the probe visits each `/demos/<featureId>` route and verifies the
copilot-chat-input selector renders.
Changes:
- `namePrefix`: `"showcase-langgraph-python"` → `"showcase-"` (full
fleet)
- `max_concurrency`: 1 → 2 (halves wall time for 17 services)
- `timeout_ms`: 600000 → 900000 (headroom for ~136 routes)
No driver code changes — the e2e-demos driver is already generic. The
infra `nameExcludes` list already drops non-demo services.
## Test plan
- [ ] Next e2e-demos tick (*/6h) produces `e2e:<slug>/<featureId>` rows
for all integrations
- [ ] Dashboard Cells tab shows D3 depth for non-LGP integrations
Relaxes the namePrefix filter from "showcase-langgraph-python" (Wave 1)
to "showcase-" (full fleet). All 17 integrations now get per-demo D3
structural checks — goto each /demos/<featureId> route + verify the
copilot-chat-input selector renders.
Also bumps max_concurrency from 1 to 2 (halves wall time for 17
services) and timeout from 600s to 900s (headroom for ~136 routes at
~5s each worst-case).
The infra nameExcludes list already drops non-demo services. No driver
code changes needed — the e2e-demos driver is already generic.
Seven concurrent useLiveStatus hooks each opened their own PocketBase
SSE connection during hydration. The PB SDK multiplexes subscriptions
over a single EventSource, but the concurrent subscribe() calls raced
the initial SSE handshake — some POST /api/realtime calls fired before
the clientId was available, returning 400. After MAX_RECONNECT_ATTEMPTS
(3), affected hooks set connection="error", triggering the "dashboard
unavailable" banner on all tabs.
Fix: replace 7 per-dimension hooks with a single useLiveStatus() call
(no dimension filter). All rows are fetched in one subscription and
the existing client-side keyFor() lookups filter by dimension
implicitly. Eliminates the 7-way race, reduces PB connection count
from 7 to 1, and removes the aggregateConnection helper entirely.
Verified live at dashboard.showcase.copilotkit.ai — zero banners, all
four tabs show real PB data with D2/D4 depth chips.
Three fixes batched to minimize PR churn:
1. Dockerfile: add ARG NEXT_PUBLIC_POCKETBASE_URL so the PB URL gets
baked into the Next.js bundle at build time. Without this, pb.ts
resolves to the sentinel URL and the dashboard shows "unavailable"
on every tab. Pre-existing bug exposed by fresh deploys.
2. showcase_deploy.yml: pass NEXT_PUBLIC_POCKETBASE_URL and
NEXT_PUBLIC_SHELL_URL as build args for the shell-dashboard service
in the CI matrix. Neither was ever passed before.
3. cell-matrix.tsx + parity-matrix.tsx: flatten nested table pattern
that caused column misalignment. Category rows used colSpan with
an inner <table> whose columns floated independently of the header.
Replaced with useCollapsible hook + flat sibling <tr> rows.
Also regenerates package-lock.json for the plugin-react downgrade
from PR #4241 (npm ci was failing in Docker).
Local Docker build verified with --build-arg for both NEXT_PUBLIC vars.
## Summary
- **ag2**: Add `max_consecutive_auto_reply=15` to `ConversableAgent` to
prevent infinite tool-call loops that flood Railway's log stream at 500+
logs/sec and starve health probes
- **agno**: Raise `OpenAIChat` timeout from default to 120s (aimock
proxy latency) and add `tool_call_limit=15` safety net
## Root Cause
**ag2 (9/12 ticks 503)**: AG2's `ConversableAgent` with
`human_input_mode="NEVER"` has no built-in turn limit. When the LLM
enters a repetitive tool-call pattern (observed: hundreds of
`get_weather(Tokyo)` calls per second), the agent loops indefinitely.
This generates 500+ log lines/sec hitting Railway's rate limit, the
agent becomes unresponsive to health probes, the watchdog kills it after
90s, and the container restarts. Cycle repeats.
**agno (3/12 ticks 503)**: The agno `OpenAIChat` model uses httpx's
default timeout, which is too short when requests route through the
aimock proxy under load. Observed `"Error in Agent run: Request timed
out."` in logs followed by agent restart. Less frequent than ag2 because
it only happens under concurrent load.
## Test plan
- [ ] Deploy ag2 and agno via CI (push to main triggers showcase_deploy)
- [ ] Monitor health for 30 min post-deploy: `curl
showcase-ag2-production.up.railway.app/api/health`
- [ ] Verify Railway logs no longer show rate-limit messages or infinite
tool-call output
- [ ] Verify watchdog kill events drop to zero
The previous commit edited package source files but didn't regenerate
the starters, causing drift-check to fail. Running generate-starters.ts
propagates the max_consecutive_auto_reply, timeout, and tool_call_limit
changes into the starter outputs.
ag2: The ConversableAgent with human_input_mode="NEVER" has no
built-in turn limit, so when the LLM repeatedly requests the same
tool call the agent loops indefinitely. This floods Railway's log
stream at 500+ logs/sec, makes the agent unresponsive to health
probes, and the watchdog kills it every ~90s. Add
max_consecutive_auto_reply=15 to cap the loop.
agno: The OpenAIChat model defaults to httpx's short timeout, which
is insufficient when requests route through aimock. Observed
"Request timed out" errors that crash the agent run. Raise timeout
to 120s and add tool_call_limit=15 as a safety net against runaway
loops.