## CopilotDrawer — promote threads management to the SDK
Implements the hardened [CopilotDrawer design (Threads
v2)](https://app.notion.com/p/3883aa3818528190b5d1f9b6ba26dca0).
Promotes thread listing/selection/creation/rename/archive/delete out of
the example apps and into the SDK as a framework-agnostic shadow-DOM web
component plus a React vertical (Angular wrapper included; Vue/RN to
follow).
> Note: assumes
[CopilotKit#5624](https://github.com/CopilotKit/CopilotKit/pull/5624) is
already merged (built on top of its `unarchiveThread` surface).
### What's in this PR (by area)
- **A — `@copilotkit/core`**: thread store with optimistic
add/rename/archive/delete (delete rolls back on reject), session-guarded
results (a superseded session can't leak an error/rollback),
`startNewThread` (non-explicit), `refetchThreads`,
`getServerState`/`getServerSnapshot`, per-store memoized selectors, and
**non-fatal** realtime diagnostics (a channel-join or
metadata-credential failure warns and keeps the already-loaded list
rather than blanking it). Registers a `"threads"` feature for catalog
gating.
- **B — `@copilotkit/web-components` (new package)**: the shadow-DOM
`<copilotkit-drawer>` Lit element — self-contained styles with
**build-time token sync** from react-core's `@theme` (checked-in
`generated-tokens.ts`, drift-guarded), slot projection for custom rows
(`slot="row:{id}"`), license/upsell gating, active/all filtering, and a
mobile modal. The element owns view-state; the host owns domain state.
Multi-format build: ESM/CJS externalize `lit` (peer dep); the **UMD
bundle inlines `lit`** so the advertised `<script>`/CDN path is
self-contained.
- **C/E/F — `@copilotkit/react-core`**: `<CopilotDrawer>` wrapper
(custom-element interop, two-pronged license gate, scoped chat-input
focus-return, registration-gated header launcher);
`CopilotChatConfigurationProvider` gains `drawerOpen` + `<768px`
modal/drawer mutual-exclusion + a **non-explicit active-thread setter**
so a bare `<CopilotDrawer>` connects to the picked thread and shows the
welcome screen on "+ New" with no host wiring; `useThreads` gains an
`{enabled}` gate and a list-only error channel.
- **D — `@copilotkit/angular`**: `injectThreads` signal wrapper —
reactive runtime context (re-dispatches when `wsUrl`/`threadEndpoints`
arrive), enabled-gated single-slot registration, bound result methods,
and error/loading parity with react-core.
### Quality
- Tests: core, web-components (46), react-core (1370), angular (106) —
all green; `tsc --noEmit` clean across all four; full builds pass; oxfmt
clean.
- **Code review:** 4 rounds of an unbiased 7-slot review fleet (15
agents/round) — 3 fix rounds + 1 confirming round that converged with
zero load-bearing findings. The convergence lever was applied per the
requester: the deep focus/overlay-accessibility rework, pagination
wiring, and Angular parity gaps are deferred to the documented
follow-ups below rather than blocking this PR.
### Follow-ups (intentionally deferred)
- **Accessibility (own PR + a11y test harness):** the mobile-modal Tab
trap now runs at the host level and contains the backdrop (fixed in
review); remaining a11y work — focus-restore to the opener on close,
scroll-lock reference-counting, confirm-delete `alertdialog` focus
management, the default-open-on-mobile scroll-lock, and a dedicated a11y
test harness.
- **Pagination wiring:** the element exposes `hasMore`/fetch-more UI but
nothing triggers it and the wrapper doesn't pass a `limit`; also clear a
recovered next-page error and cancel an in-flight next-page on filter
change.
- **Error surfacing:** wire `onError` to a transient toast and separate
the mutation-error channel from the list-load error (the list no longer
blanks on a failed mutation — the rollback restores the row — but
mutation failures are currently silent beyond the rejected promise).
- **Angular parity:** mirror react-core's `startNewThread` config-error
dismissal, add a list-only `listError`, and refresh
intelligence/threadEndpoints in `updateRuntime` on a runtime-URL change.
- **Reach:** Vue + React Native wrappers; de-fork the ~18 example apps
onto the SDK drawer; nested-provider active-thread sharing.
- **Minor:** migrate the react-core/angular wrappers to per-store
`store.selectors`; honor-or-drop `ThreadEnvironment.fetch` (RxJS
`fromFetch` ignores it; both wrappers pass `globalThis.fetch`, so no
behavior change today); `threadId=""` controlled-predicate edge; reveal
animation for slotted rows; empty-string-timestamp coalescing;
`_visibleThreads` recompute; empty-name `aria-label`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The mobile drawer is a modal dialog, but the Tab trap was bound to .root only,
while the backdrop button renders as a sibling OUTSIDE .root — so Tab from the
backdrop (or any out-of-root node) could escape the modal. Move the trap to the
host keydown listener (keydown is composed, so it catches the backdrop too) and
include the backdrop in the focusable cycle; keep initial focus on the first
real control. Adds a regression test for backdrop/outside-root Tab containment.
The UMD bundle inlines lit for a self-contained CDN <script>; inlining lit also
pulls in its runtime deps (@lit/reactive-element, lit-html, lit-element,
@lit-labs/ssr-dom-shim). tsdown flags that transitive bundling as 'unintended'
and promotes the warning to a fatal error under CI, breaking
@copilotkit/web-components:build (and every dependent). Declare all lit packages
in noExternal and set inlineOnly: false to mark the bundling intentional.
Angular signal wrapper over the core thread store: reactive runtime context
(re-dispatches when wsUrl/threadEndpoints arrive), enabled-gated single-slot
registration, bound result methods, and synthesized error/loading parity with
react-core.
Add <CopilotDrawer> (interops with the shadow-DOM element, two-pronged license
gate, scoped chat-input focus return, registration-gated header launcher) and
extend CopilotChatConfigurationProvider with drawerOpen + mobile mutual
exclusion + a non-explicit active-thread setter so a bare drawer connects to the
picked thread and resets on New with no host wiring. useThreads gains an
{enabled} gate and a list-only error channel.
Shadow-DOM Lit element that renders the threads drawer: self-contained styles
with build-time token sync from react-core's theme, slot projection for custom
rows, license/upsell gating, filtering, and a mobile modal. View-state lives on
the element; domain state stays with the host. Multi-format build (ESM/CJS
externalize lit as a peer dep; UMD inlines lit so the CDN <script> path is
self-contained).
Add the thread store (optimistic add/rename/archive/delete with delete
rollback-on-reject, session-guarded results, startNewThread/refetchThreads,
getServerState/getServerSnapshot, per-store memoized selectors, and non-fatal
realtime diagnostics for channel-join and metadata-credential failures) and
register the "threads" feature so the SDK can gate thread management.
## What
Makes the Slack triage agent runtime (`examples/slack/runtime.ts`)
resilient to MCP server failures. A down, misconfigured, or hanging MCP
server no longer takes the whole agent down.
## Why
The factory connected its MCP clients with `Promise.all`, and
`@tanstack/ai-mcp`'s `createMCPClient` connects eagerly. So a single
unreachable server (bad API key, sidecar not running, network hang)
rejected the entire `Promise.all`, the factory threw, and the run
surfaced as a fatal:
```
⚠️ Agent error: Failed to connect to MCP server
```
The bot was effectively dead for **every** request — even ones that need
no MCP at all (plain chat, web search, rendering). This was hit in
practice when `.env` carried a placeholder `LINEAR_API_KEY` / missing
Notion sidecar.
## How
- Connect each MCP server **independently** via `Promise.allSettled`,
each wrapped in an 8s timeout (a hanging server can't stall the turn; a
late post-timeout failure is swallowed so it can't become an unhandled
rejection).
- **Drop** the servers that fail; the agent runs with whatever's left —
web search, card/chart rendering, thread reading, and any MCP that did
connect.
- Inject a per-turn system note listing the unavailable sources,
instructing the model to mention the outage **only if the user's request
needs that source**, and never to invent data or claim a write
succeeded.
- Connections are re-attempted each turn, so a transient outage
**self-heals** on the next message.
## Testing
- `pnpm check-types` on the example — clean for `runtime.ts` (one
pre-existing unrelated error in `app/demo-restart.tsx` re: unbuilt
`@copilotkit/bot-store-redis`).
- Manual: ran the example with a deliberately bad `LINEAR_API_KEY`.
General questions answer normally (Linear dropped, logged `MCP "Linear"
unavailable this turn`); asking for Linear yields a graceful
"temporarily unreachable" instead of a fatal agent error.
The triage runtime connected its MCP clients with Promise.all, so a single
unreachable/misconfigured server (bad key, sidecar down, hang) rejected the
whole run and surfaced as a fatal "⚠️ Agent error: Failed to connect to
MCP server" — the bot was dead for every request, even ones needing no MCP.
Connect each server independently (Promise.allSettled) with an 8s timeout,
drop the ones that fail, and let the agent run with whatever's left (web search,
rendering, thread reading, and any MCP that did connect). A per-turn system note
lists any down sources so the model only tells the user a source is unreachable
if they actually ask for it, and never invents data. Connections are retried
each turn, so a transient outage self-heals.
## What
Replaces the homemade animated `⏳` "thinking"
placeholder (a posted message whose dots cycled via `setInterval`) with
Slack's **native** `assistant.threads.setStatus` "is thinking…"
indicator — and generalizes it from assistant-pane-only to **every
thread-anchored reply**: channel @-mentions, channel threads, DMs, and
the pane.
## Why
The hourglass was an extra post-then-delete message that looked nothing
like the native loading state other Slack agents show. Slack has since
relaxed `assistant.threads.setStatus` to accept the ordinary
**`chat:write`** scope (not just `assistant:write`), specifically so
channel-based apps can show AI loading states in channels and DMs
([docs](https://docs.slack.dev/reference/methods/assistant.threads.setStatus/)).
The adapter already used `setStatus` for the pane — this just extends it
everywhere and deletes the placeholder.
## How
- **`event-renderer.ts`** — delete the hourglass machinery
(`startThinking`/`claimThinking`/`clearThinking`/`setInterval`);
generalize `setPaneStatus`/`clearPaneStatus` →
`setStatus`/`clearStatus`. New `status?: { threadTs, isPane, config }`
arg: `statusMode` drives the thinking indicator; `isPane` only selects
tool-progress surface (pane composer status vs `task_update` timeline /
`🔧` rows — both unchanged).
- **`adapter.ts`** — `createRunRenderer` builds `status` for any target
with a thread anchor (`threadTs ?? statusTs`); `assistant: false` opts
out everywhere.
- **`types.ts` / `slack-listener.ts`** — flat DMs (no `thread_ts`) carry
the inbound message `ts` as the status anchor; replies still post flat.
- **README** — document the broadened status + the `chat:write`
relaxation.
## Testing
- `nx run-many -t test build` for `bot-slack` — **248 tests pass**,
build clean, oxlint 0 errors, oxfmt clean.
- New tests cover non-pane channel-thread status, the `🔧` tool
path staying on non-pane, and the DM `statusTs` anchor.
- Net **−149 lines**.
> Note: the DM path anchors `setStatus` to a non-assistant DM message ts
— the one piece only verifiable against the live Slack API. Channel
@-mentions already carry a real `thread_ts`.
Replace the homemade animated ⏳ placeholder (a posted
message with setInterval-cycled dots) with Slack's native
assistant.threads.setStatus "is thinking..." indicator on every thread-anchored
reply -- channel @-mentions, channel threads, DMs, and the assistant pane -- not
just the pane. Slack now accepts setStatus under chat:write (not only
assistant:write), so it works for channel-based apps. Flat DMs carry the inbound
message ts as the status anchor. assistant:false opts out everywhere.
## What
Bumps the canonical CopilotKit pin across **all showcase integrations +
shell** from `1.61.1` to `1.61.2`, and adopts the new DX from
[CopilotKit#5611](https://github.com/CopilotKit/CopilotKit/pull/5611) in
the A2UI dynamic demos.
`1.61.1` (published 2026-06-23 21:28) predates the #5611 merge
(2026-06-24 13:36), so the feature could not reach showcase without a
release. `1.61.2` carries it (verified `providerA2UIHasCatalog` in
runtime + `a2uiCatalogAvailable` in react-core).
## Changes
- **Version bump** — `showcase/scripts/showcase-canonical-pins.json`
`1.61.1` → `1.61.2`; every `@copilotkit/*` pin in 22 `package.json` + 22
`package-lock.json` regenerated to match.
- **Feature adoption** — removed the now-redundant runtime `a2ui` block
(`injectA2UITool: true` + `defaultCatalogId:
"declarative-gen-ui-catalog"`) from the **declarative-gen-ui (A2UI
dynamic)** route of:
- `langgraph-python`, `langgraph-fastapi`, `langgraph-typescript`
- `strands`, `strands-typescript`
- `google-adk`
The catalog passed to the provider (`<CopilotKit a2ui={{ catalog }}>`)
now auto-enables A2UI and defaults tool injection on. The forwarded
catalog supplies its own `catalogId` (the sdk-js A2UI middleware
auto-derives `defaultCatalogId` from it), so the old "Catalog not found"
fallback no longer applies. Stale comments updated accordingly.
## Verification
- `validate-pins` drift ratchet **unchanged** (count 38, same hash) —
`@copilotkit/*` pins are not in the FAIL set, so no re-baseline needed.
- **langgraph-python D6 `gen-ui-declarative`: green** end-to-end (real
container, aimock replay) with the runtime `a2ui` config removed — no
"Catalog not found".
- strands D6 (runtime-middleware path) verification in flight; langgraph
siblings + ADK covered by parity (identical edit + middleware path).
The 2 reds in the langgraph-python D6 run (`auth`,
`gen-ui-open-advanced`) are unrelated demos with timing-based flakiness,
not affected by this change.
Bump the canonical CopilotKit pin across all showcase integrations + shell
to 1.61.2 (canonical-pins.json, every package.json + package-lock.json),
which carries CopilotKit#5611: passing a catalog to the provider
(`<CopilotKit a2ui={{ catalog }}>`) now auto-enables A2UI and defaults tool
injection on, so the runtime no longer needs an explicit `a2ui` config.
Demonstrate the feature on the A2UI dynamic (declarative-gen-ui) demos by
removing the now-redundant runtime `a2ui` block (`injectA2UITool: true` +
`defaultCatalogId`) from:
- langgraph-python, langgraph-fastapi, langgraph-typescript
- strands, strands-typescript
- google-adk
The forwarded catalog supplies its own catalogId (sdk-js A2UI middleware
auto-derives `defaultCatalogId` from it), so the previous "Catalog not found"
fallback no longer applies.
Verified: validate-pins drift ratchet unchanged (38 / same hash);
langgraph-python D6 `gen-ui-declarative` green end-to-end (no Catalog-not-found).
## Release monorepo v1.61.2
**Scope:** `monorepo` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.61.2`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `monorepo` packages to npm at version `1.61.2`
- Creates git tag `monorepo/v1.61.2`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
## Release bot-slack v0.1.0
**Scope:** `bot-slack` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `bot-slack` packages to `0.1.0`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `bot-slack` packages to npm at version `0.1.0`
- Creates git tag `bot-slack/v0.1.0`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
## Summary
Fixes the two RED A2UI D6 demos in the **built-in-agent** showcase
integration. Both are confirmed GREEN locally via the D6 control-plane
probe (`--d6 --direct --isolate`).
### a2ui-fixed-schema — React #31 crash → GREEN
**Root cause:** the showcase authors A2UI catalog defs with root
`zod@4`, but `@a2ui/web_core`'s `GenericBinder` schema scraper inspects
**Zod-3** internals (`_def.typeName==='ZodUnion'`). A zod@4 union
reports `_def.typeName===undefined` → field misclassified STATIC → the
raw `{path}` binding object reaches render → **React error #31**.
**Fix:** author this demo's catalog with a `zod-v3` (`npm:zod@3.25.76`)
alias so the binder resolves bindings (card now renders `SFO → JFK,
$289`). NOT a react-core fix.
### declarative-gen-ui — surface-missing (no paint) → GREEN
**Root cause:** the secondary-LLM prompt was far thinner than the
canonical generation guidelines, so it emitted trees that (correctly)
failed the renderer's `surfaceHasRenderableContent` paint gate. The
paint gate is correct and untouched.
**Fix:** port the canonical generation rules into the prompt, add output
validation, add catalog parity (`DataTable` + info-row), ground the
planner with sales-context, and record multi-turn aimock fixtures (4/4
turns paint).
## Red→green
- a2ui-fixed-schema: RED `Minified React error #31 {path}` → GREEN `✓
d6:built-in-agent green`, card resolves real values.
- declarative-gen-ui: RED `reason=surface-missing` → GREEN
`turnsCompleted:4, 1 passed`, all sales pills paint
(KPI/pie/bar/DataTable/status/info-row).
## Cross-regression
The shared-renderer path was checked across A2UI demos on both runtimes:
gen-ui-agent ✓, beautiful-chat 5/5 ✓, langgraph-python (v1)
a2ui-fixed-schema ✓ — no regressions.
## Code review
2 review rounds (7 agents each). Applied: a2ui factory validation
hardening (plain-object data guard, unique-id check, fail-loud on
non-string secondary-LLM return), two-arg `z.record` (zod@4 API),
index-based DataTable row key, Metric `trendValue` rendering for neutral
trend, doc corrections.
## Follow-ups (not this PR)
- **openai@5 / zod@4 peer ERESOLVE** on fresh `npm install`
(pre-existing; committed lock installs via `npm ci`).
- Cross-integration `sales-context.ts` sync (5 byte-identical copies).
- A2UI tree cycle/reachability enforcement (deferred — not a realistic
LLM-output threat; unique-id IS enforced).
- Non-string component-id edge in the unique-id guard.
- `SYSTEM_PROMPT` catalog enumeration omits `DataTable`.
- `page.tsx` docstring drift; DonutChart legend mirror; Row `align` map;
fixture match-key coupling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The secondary-LLM prompt was far thinner than the canonical generation guidelines,
so it emitted trees that (correctly) failed the renderer's paint gate → surface-missing.
Port the canonical generation rules into the prompt, add output validation, add catalog
parity (DataTable + info-row), ground the planner with sales-context, and record
multi-turn aimock fixtures. Includes CR fixes: two-arg z.record for the DataTable rows
schema (zod@4 API), index-based DataTable row key, and Metric trendValue rendering for
neutral trend.
The showcase authors A2UI catalog defs with root zod@4, but @a2ui/web_core's
GenericBinder schema scraper inspects Zod-3 internals (_def.typeName==='ZodUnion').
A zod@4 union reports _def.typeName===undefined → misclassified STATIC → the raw
{path} binding object reaches render → React error #31. Author this demo's catalog
with a zod-v3 (npm:zod@3.25.76) alias so the binder resolves bindings. Includes CR
hardening of the shared a2ui factory validation (plain-object data guard, unique-id
check, fail-loud on non-string secondary-LLM return).
## What this is
The consolidated, Jerel-owned PR for the **SaaS banking demo**
(FOR-137), built on top of the now-merged **#5180** (Maxim, FOR-138). It
continues the closed draft **#5183**, rebuilt cleanly on current `main`.
Scope: **53 files, entirely within `examples/showcases/banking` +
`examples/showcases/teach-mode`.**
<img width="1358" height="763" alt="image"
src="https://github.com/user-attachments/assets/2ad50912-077e-4a03-821b-047e487a7c8d"
/>
<img width="1600" height="894" alt="image"
src="https://github.com/user-attachments/assets/f80d894f-a24c-4d77-af83-0d1cf1369b18"
/>
<img width="800" height="451" alt="image"
src="https://github.com/user-attachments/assets/c8d6abe5-2e70-4ad1-87af-8aa6529906dc"
/>
## What's added (on top of #5180)
- 🎨 **Aurora design system** — premium light/violet fintech redesign
(lavender canvas, violet→indigo brand, gradient cards, hand-rolled SVG
statistics sparkline, Inter). Restyle-only; all wiring preserved.
- 🪟 **Chat as a docked side panel + Threads inbox** — v2 `useThreads`;
the chat is a right-docked `CopilotSidebar`; Threads live as an **inbox
button in the panel header** (conversation list, new / archive /
select).
- 🧠 **Self-learning teachable gate** — approving an over-limit
transaction is blocked with a **symptom-only** error; a human unlocks it
by filing a **justifying** policy exception; the agent gets tools +
distractors but **never the unlock recipe**. OSS mode is the default;
`CopilotKitIntelligence` is env-gated.
- 📘 **Reusable teach-mode cookbook** — `examples/showcases/teach-mode/`
(the 5-role teachable-loop contract + banking↔e-commerce mapping +
adoption checklist + recording seam + `verify-teachable-gate.sh`).
- 🗺️ **Learning-track plan** — `LEARNING-TRACK-PLAN.md`: how to make the
live learn→distill→retrieve loop work + the teachable-demo UX (suggested
prompt, inline HITL, recording vignette).
## Status / notes
- **Draft.** Runs in **OSS mode** (only `OPENAI_API_KEY`); no
Intelligence backend required.
- The **live** self-learning loop
(record→distill→`/knowledge`→fresh-agent-learns) is gated on the
react-core recording hook (`e103a19`) + an Intelligence backend — see
`LEARNING-TRACK-PLAN.md`. The teachable **mechanics** (gate + unlock)
work today and are REST-verifiable (`verify-teachable-gate.sh`).
- cc @mxmzb — consolidated demo PR per Sam's ask; builds directly on
your merged #5180.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Doc-only single-file correction to
`showcase/integrations/built-in-agent/PARITY_NOTES.md`. The
`gen-ui-agent` D6 cell already passes end-to-end locally, but its
PARITY_NOTES entry still documented it as RED/blocked on a `STATE_DELTA
→ useAgent` gap in `@copilotkit/react-core`. That premise is stale and
is now refuted by local D6 runs. This rewrites the entry to
GREEN/reclaimed and rescopes the surrounding section header to the
remaining A2UI render-layer demos.
## RED → GREEN proof (from the work log)
The "RED" here is documentary, not behavioral: the cell **passes**
despite the stale RED doc.
Local RED baseline (`bin/showcase test built-in-agent:gen-ui-agent --d6
--direct --isolate`):
```
[conversation-runner] turn 3/3 — assertions passed
[conversation-runner] conversation completed successfully { turnsCompleted: 3 }
✓ d6:built-in-agent green (44.1s)
1 passed (44.1s)
```
Local GREEN value-test (same command, `--repeat 3`):
```
3 passed (130.1s)
✓ Tests passed for built-in-agent:gen-ui-agent
```
3/3 stable — not a flake. The doc correction changes no runtime
behavior.
Why it works: the backend `set_steps` server-tool result is converted to
a `STATE_DELTA` `[{op:"add", path:"/steps", value:steps}]` in
`src/lib/factory/tanstack-factory.ts` (`add`, not `replace`, so
`@ag-ui/client@0.0.57` doesn't drop it as
`OPERATION_PATH_UNRESOLVABLE`). `@ag-ui/client` applies the patch and
fires `onStateChanged`; the core state-manager fans it to subscribers;
`useAgent` re-renders off `agent.state.steps`. Fully wired in published
1.61.1 — no react-core change needed.
## Key finding: there was NO config quarantine
`gen-ui-agent` was never in the manifest `not_supported_features`, never
excluded in `shared/constraints.yaml`, and
`shared/feature-registry.json` has no per-feature status field. The
harness already routes it into `runnable` and grades it green. The
**stale doc was the only artifact** — there was no executable quarantine
to lift, so this is a pure doc correction.
## The two genuinely-RED demos are out of scope
`a2ui-fixed-schema` (React #31 crash from an unresolved `{path}` A2UI
binding) and `declarative-gen-ui` (surface never paints; secondary-LLM
op-shape) are real bugs, but both belong to `@copilotkit/a2ui-renderer`
/ showcase — **not** `@copilotkit/react-core`. They are being addressed
in a separate spec and are intentionally left untouched here.
## Test plan
- [x] `oxfmt --check` on the changed file — passes (correctly formatted)
- [x] `oxlint` on the changed file — 0 warnings, 0 errors
- [x] `commitlint` on the commit message — passes (`docs(showcase):`)
- [x] D6 cell passes locally, 3/3 stable
- [ ] CI green
## Problem
The CopilotKit Interactive Dojo (`showcase/shell-dojo`) built its
preview-iframe `src` from `integration.backend_url`, which
`scripts/generate-registry.ts` synthesizes into `registry.json` at
**Docker build time** (default
`showcase-{slug}-production.up.railway.app`). The value is therefore
frozen into every image regardless of environment — so the **staging
dojo iframed production integration backends**.
## Root cause
The shell (`showcase/shell`) already solved this in SU-13 (commit
`febd8bc9`, 2026-06-11): it derives each integration's backend host at
**request time** from `SHOWCASE_BACKEND_HOST_PATTERN` via
`lib/backend-url.ts`, keeping `registry.json` as the source for non-URL
metadata only. That fix was scoped to `shell` and **never ported to
`shell-dojo`** — `shell-dojo`'s `RuntimeConfig` was still empty and
`page.tsx` read the build-baked `backend_url`.
## Fix
Port the `backendHostPattern` slice of SU-13 to `shell-dojo` (nothing
else from SU-13 — the dojo has no docs-redirect middleware, posthog, or
baseUrl consumers):
- **`lib/backend-url.ts`** — verbatim copy of shell's
(`resolveBackendUrl` + the `NEXT_PUBLIC_LOCAL_BACKENDS` local-dev
override). The two apps are separate build roots with different package
managers (shell is a pnpm workspace member; shell-dojo is a standalone
`npm ci` Next app, same as shell-dashboard), so neither can import the
other's `src`, and there is no shared importable package — the same
reason `generate-registry.ts` already keeps its own copy. A drift-guard
test keeps the copy byte-identical and pins the default pattern across
`backend-url.ts` and `generate-registry.ts`.
- **`lib/runtime-config.ts`** — adds `backendHostPattern`, read from
`SHOWCASE_BACKEND_HOST_PATTERN` at request time (with the
`NEXT_PUBLIC_`-prefixed/trim fallback semantics shared with shell).
Unset → the prod default, so an unset deploy is byte-identical to today.
- **`lib/runtime-config.client.ts`** — carries the SSR sentinel
`showcase-{slug}.ssr-placeholder.invalid` + a required-field check.
- **`app/page.tsx`** — `previewUrl` now derives via
`resolveBackendUrl(...)` at request time, **gated on a `mounted` flag**
so the SSR-phase sentinel host never reaches an iframe `src`. shell-dojo
loads its registry synchronously (`getIntegrations()` in a `useMemo`,
`viewMode` defaults to `"preview"`), so unlike the shell it has no
client-`useEffect` data guard to defer the read past hydration — without
the gate, the iframe would render on first paint with the `.invalid`
sentinel host (a request to a dead domain + a flash before the real URL
swaps in).
- **`app/layout.tsx`** — comment-only; the existing generic
`JSON.stringify(config)` injection already carries the new field into
`window.__SHOWCASE_CONFIG__`.
## Ops
`SHOWCASE_BACKEND_HOST_PATTERN=showcase-{slug}-staging.up.railway.app`
is already set on the **staging dojo** Railway service (verified the
staging hostname convention against a live staging integration's
`RAILWAY_PUBLIC_DOMAIN`). **Production is intentionally left unset** =
the prod default pattern, so prod behavior is unchanged. The var is
runtime-only (images build in CI, not on Railway), so it was inert until
this lands — merging this is what flips staging off prod backends.
## Testing
- New drift-guard test (in `scripts`): `backend-url.ts` byte-identical
to shell's + default-pattern parity with `generate-registry.ts` — 2/2
pass.
- Full affected vitest run: 2110/2110 pass.
- Standalone shell-dojo Next build (`npm ci --ignore-scripts && npm run
build`) compiled + type-checked successfully.
- oxfmt clean; oxlint 0 errors.
## Notes for reviewers
- **shell-dojo is not in the pnpm workspace**, so `nx affected` does not
cover it — CI builds it via its own npm/Docker path. Verify locally with
`cd showcase/shell-dojo && npm ci && npm run build`.
- The `iframe-missing-sandbox` oxlint **warning** at `page.tsx`
pre-dates this change (the iframe does carry a `sandbox` attr; oxlint
can't see it through the variable) and is outside the diff's intent —
left untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The built-in-agent gen-ui-agent D6 cell already passes end-to-end locally;
the PARITY_NOTES entry that documented it as RED/blocked on a STATE_DELTA
to useAgent gap in @copilotkit/react-core was stale. The set_steps to
STATE_DELTA {op:"add", path:"/steps"} workaround merged in
tanstack-factory.ts closed that gap: @ag-ui/client applies the patch and
fires onStateChanged, the core state-manager fans it to subscribers, and
useAgent re-renders off agent.state.steps. No react-core change is needed.
Rewrites the gen-ui-agent entry to GREEN/reclaimed and rescopes the
section header to the remaining A2UI render-layer demos (a2ui-fixed-schema,
declarative-gen-ui), whose fixes belong to @copilotkit/a2ui-renderer, not
react-core. Doc-only; no config quarantine existed (gen-ui-agent was never
in manifest not_supported_features), so the cell stays a counted green.
Local RED baseline: cell passes (1 passed) despite the stale RED doc.
Local GREEN value-test: --repeat 3 => 3 passed (130.1s), stable.
The dojo's preview iframe built its src from `integration.backend_url`,
which generate-registry.ts bakes into registry.json at Docker BUILD time
(default `showcase-{slug}-production.up.railway.app`). So the staging
dojo iframed PROD integration backends — the exact staging->prod leakage
the shell's SU-13 runtime-derivation fix already prevents, but which was
never ported to shell-dojo.
Port the `backendHostPattern` slice of SU-13:
- copy shell's backend-url.ts verbatim (resolveBackendUrl + the
NEXT_PUBLIC_LOCAL_BACKENDS local-dev override); a scripts drift-guard
test keeps it byte-identical to the shell's and pins the default
pattern across backend-url.ts and generate-registry.ts.
- add `backendHostPattern` to shell-dojo's RuntimeConfig (server reads
SHOWCASE_BACKEND_HOST_PATTERN at request time; client carries the SSR
sentinel) — the existing layout injection picks it up automatically.
- page.tsx derives previewUrl via resolveBackendUrl at request time,
gated on a `mounted` flag so the SSR-phase sentinel host never reaches
an iframe src (shell-dojo loads the registry synchronously, so unlike
the shell it has no data-loading guard to defer the read past
hydration).
Staging dojo's SHOWCASE_BACKEND_HOST_PATTERN is set to
`showcase-{slug}-staging.up.railway.app`; prod stays unset (= default
prod pattern), so prod behavior is byte-identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
`showcase/aimock/RAILWAY.md` described the aimock fixture infrastructure
as it worked during an earlier migration phase, not as it works today.
The stale claims actively misled debugging. This PR corrects them to
match the real, verified infra.
## Before → After
| Claim | Before (stale) | After (correct) |
| --- | --- | --- |
| Fixture delivery (§4/§5) | Three fixtures fetched at boot from
GitHub-raw URLs: `d5-all.json`, `smoke.json`, `feature-parity.json` |
Fixtures are **baked into the image** at build time under
`/fixtures/{shared,d4,d6}`; no remote fetch |
| `d5-all.json` | Authoritative D5 bundle, re-bundled from
`harness/fixtures/d5/*` | **Gone** — a one-time migration source split
into the per-slug `showcase/aimock/d6/<slug>/` tree (the source of
truth) |
| startCommand `--fixtures` (§5) | 3 `raw.githubusercontent.com` URLs |
`--fixtures /fixtures/shared --fixtures /fixtures/d4 --fixtures
/fixtures/d6` (the baked-in dirs). Added a warning that a single
`--fixtures /fixtures` parent loads nothing |
| Runtime image (§3) | Bare `ghcr.io/copilotkit/aimock:<version>` pulled
directly | `showcase-aimock` image, `FROM
ghcr.io/copilotkit/aimock:latest` + baked fixtures, built by
`showcase_build.yml` |
| Dockerfile (§8) | "Dead code / legacy wrapper, safe to remove" |
**LIVE** — `showcase_build.yml` builds it (`dockerfile:
showcase/aimock/Dockerfile`) and it bakes `shared/`, `d4/`, `d6/` into
`/fixtures/`. Do not remove |
| Env vars (§6), references (§9) | Referenced public GitHub-raw fixture
URLs and ~5 min raw-edge cache propagation | Local baked dirs;
propagation is via image rebuild + Railway deploy |
Also documents the working Railway mutation auth path by mechanism only
(§2): account-scoped `RAILWAY_TOKEN` from the DevOps `showcase`
1Password item via GraphQL `Authorization: Bearer` — the CLI session
token is not authorized for showcase-project mutations. No token value
is in the doc.
## Verification
- `showcase/aimock/d5-all.json` does not exist; the tree is `shared/`,
`d4/`, `d6/`.
- `showcase/aimock/Dockerfile` is `FROM
ghcr.io/copilotkit/aimock:latest` + `COPY shared|d4|d6 ->
/fixtures/...`.
- `.github/workflows/showcase_build.yml` matrix entry `showcase-aimock`
builds `dockerfile: showcase/aimock/Dockerfile`, context
`showcase/aimock`.
Docs-only change. Do not merge / do not enable auto-merge.
## Summary
Corrects the `pydantic-ai` `gen-ui-declarative` aimock fixture. PR #5661
merged it mis-templated from `ms-agent-dotnet`: the inner secondary-LLM
blocks gated on `_design_a2ui_surface` (ms-agent's tool) instead of
pydantic-ai's actual **`render_a2ui`**, and only **1 of the 4**
declarative pills was covered (the rest were dead KPI/pie/bar/status
blocks that match no real pill). So the cell still 503'd.
Re-authored 1:1 from the canonical `langgraph-python` fixture: inner
tool **`render_a2ui`**, all **4 pills** (sales-dashboard, rep-vs-quota,
at-risk, biggest-account) as outer `generate_a2ui` + inner `render_a2ui`
+ tool-result triplets, `context: "pydantic-ai"`. Dead blocks removed.
## Effect (real-surface proven via `bin/showcase test
pydantic-ai:declarative-gen-ui --d6 --direct`)
- `no_fixture_match` 503 count **6 → 0** — the 503 is fully gone.
- **Necessary, not sufficient:** the cell still can't paint (advances
503 → `surface-missing`) due to **separate pydantic-ai integration
gaps** — missing `DataTable` renderer, `InfoRow` lacking the
`declarative-info-row` testid, and the `injectA2UITool: false` A2UI
delivery path. Filed as a follow-up (Slack `#` showcase alerts); out of
scope here.
- No-regression: `multimodal` cell still green; fixture-only change
(+187/-336, one file).
## Test plan
- [x] Real-surface red-green: `no_fixture_match` 6 → 0
- [x] Schema valid (`validateFixtures` 0 issues), no fixture shadowing
(zero delta)
- [x] Inner `render_a2ui` arguments byte-identical to canonical
langgraph-python
- [ ] CI green
## Problem
The Slack example currently registers an emoji-triage reaction handler,
so adding reactions can trigger an agent run.
## Why
The example should model explicit conversational triggers only.
Reactions should not start generations in this example.
## Fix
Remove the example reaction handler, reaction demo docs/tests, and Slack
manifest reaction scopes/events.
## Verification
- pnpm exec nx run slack-example:test
- pnpm exec nx run slack-example:check-types
- pnpm exec nx run slack-example:build
- rg -n
"emojiTriage|bot.onReaction|reaction_added|reaction_removed|reactions:read|reactions:write|emoji
triage" examples/slack -S
- git diff --check -- examples/slack
## Root cause: manifest gap makes #5673 a no-op on the fleet
PR #5673 fixed the aimock D6 fixture for the `shared-state-read` cell
for the
`strands` and `strands-typescript` integrations. But that fix was a
**no-op on
the deployed fleet**, because neither integration's manifest declared a
`demos:`
entry for `shared-state-read`.
The fleet enumerates D6 cells **only** from manifest demos that have
both an `id`
**and** a `route`:
- `showcase/harness/src/cli/targets.ts` `demosForSlug()` maps
`manifest.demos` →
ids; the d6 all-pills driver derives its feature matrix from this (an
empty
array short-circuits to a zero-cell false-green).
- `showcase/scripts/generate-registry.ts` `determineCellStatus()`: a
feature
declared in `features` but with no demo carrying a `route` → status
`unshipped`.
- `showcase/harness/src/probes/discovery/railway-services.ts`: only
demos with a
string `id` + `route` are counted.
`shared-state-read` **was** declared as a `feature` in both manifests
and is
**not** in `not_supported_features`, and the frontend demo (`page.tsx`,
`recipe-card.tsx`, `types.ts` — byte-identical to langgraph-python)
already
exists on disk. Only the manifest `demos:` entry was missing → status
`unshipped` → the cell never ran on staging.
## The fix (mirror the gold standard)
Add the `shared-state-read` demo entry to **both** manifests, mirroring
the
gold-standard `langgraph-python` entry exactly (same id, name,
description, tags,
route, `animated_preview_url`, highlight files). The gold entry already
uses the
integration-neutral default-agent shape (no agent file in `highlight`,
just
`page.tsx` + `api/copilotkit/route.ts`), so no per-integration
adjustment was
needed.
```yaml
- id: shared-state-read
name: "Shared State: Read-only"
description: "Recipe editor publishes form state via agent.setState; the agent reads the recipe context but does not mutate it (no backend tool — neutral default agent)."
tags:
- agent-state
route: /demos/shared-state-read
animated_preview_url:
highlight:
- src/app/demos/shared-state-read/page.tsx
- src/app/api/copilotkit/route.ts
```
No generated artifact is committed (`registry.json` / `catalog.json` /
`constraints.json` are gitignored, regenerated at build/test time). No
hard-coded
count test needs updating: the cross-join total stays 920 cells (only
the
**status** of 2 existing cells flips `unshipped → wired`);
`generate-catalog`'s
13 tests pass.
## RED → GREEN proof (real surface, not a unit fake)
### Harness enumeration (`demosForSlug` — the exact fn the d6 driver
uses)
| Integration | RED (origin/main) | GREEN (this PR) |
|---|---|---|
| strands | count=38, includes-ssr=**false** | count=39,
includes-ssr=**true** |
| strands-typescript | count=38, includes-ssr=**false** | count=39,
includes-ssr=**true** |
### Catalog status (`generate-registry` `determineCellStatus`)
| Integration | RED (origin/main) | GREEN (this PR) |
|---|---|---|
| langgraph-python (gold) | wired (max_depth=4) | wired (max_depth=4) |
| strands | **unshipped** (max_depth=0) | **wired** (max_depth=4) |
| strands-typescript | **unshipped** (max_depth=0) | **wired**
(max_depth=4) |
### Full D6 fleet run (`bin/showcase test <slug>:shared-state-read --d6
--verbose --isolate`)
On `origin/main` these resolve `count=0` (cell absent from the run set).
With this PR:
**strands:**
```
cli.runner.resolved-targets {"target":"strands:shared-state-read","level":"d6","count":1}
fleet.control-plane.catalog-enumerated {"driverKind":"e2e_d6","discovered":1,"enqueueable":1}
✓ d6:strands/shared-state-read green (0.0s)
1 passed (0.0s)
✓ Tests passed for strands:shared-state-read
```
**strands-typescript:**
```
cli.runner.resolved-targets {"target":"strands-typescript:shared-state-read","level":"d6","count":1}
fleet.control-plane.catalog-enumerated {"driverKind":"e2e_d6","discovered":1,"enqueueable":1}
1 passed (0.0s)
✓ Tests passed for strands-typescript:shared-state-read
```
Related: #5673 (the fixture fix this PR activates on the fleet).
PR #5661 mis-templated this fixture from ms-agent-dotnet: the inner
secondary-LLM blocks used toolName "_design_a2ui_surface" (never matches
pydantic-ai, whose inner tool is render_a2ui) and only the sales-dashboard
pill had any block, so the other 3 declarative pills matched nothing and
the cell 503'd with no_fixture_match on turn 1.
Re-author 1:1 from the canonical langgraph-python fixture (identical inner
tool render_a2ui + per-pill surfaces), adapted for pydantic-ai: context
"pydantic-ai" on the outer generate_a2ui + narration entries, inner
render_a2ui entries matched by toolName alone (the agent's inner OpenAI()
client does not forward x-aimock-context). All 4 pills (sales-dashboard,
team-performance, at-risk, top-account) now get both an outer
(generate_a2ui) and inner (render_a2ui) block whose component payloads meet
each pill's probe assertion. Dead KPI/pie/bar/status blocks removed.
Real-probe proof: with this fixture the inner render_a2ui call matches and
the outer narration renders ("Here's your Q2 sales dashboard.") — the cell
advances from "Strict mode: no fixture matched / 503" to all LLM calls
matched. Remaining surface-missing failure is a non-fixture frontend/agent
A2UI delivery gap (see PR description), out of scope for this fixture fix.
Add the shared-state-read demo entry to the strands and strands-typescript
manifests, mirroring the gold-standard langgraph-python entry. The fleet
enumerates D6 cells only from manifest demos that have both an id and a
route; shared-state-read was declared as a feature (and is not in
not_supported_features) but had no demo entry, so it resolved to status
unshipped and never ran on staging.
This makes the aimock fixture fix from #5673 actually take effect on the
fleet: both integrations now enumerate and run the shared-state-read cell
green.
## Summary
- Add Slack `respondTo` routing options with mention-only channel thread
replies by default.
- Keep DMs and assistant pane conversational while allowing legacy
owned-thread continuation by config.
- Update the Slack example config, manifests, docs, and Discord README
trigger wording.
## Why
Slack bots should stay quiet in channel threads unless explicitly
mentioned or configured for continuation. This makes Kite-style bots
less noisy by default while preserving the old behavior for teams that
want it.
## How
- Resolve `respondTo` once in `SlackAdapter.start()` and pass the
resolved policy into the Slack listener.
- Gate `app_mention`, `message.im`, and non-DM thread replies against
that policy.
- Add listener coverage for defaults, disabled routes, in-channel
mention replies, and legacy thread continuation.
## Test Plan
- `pnpm exec nx run @copilotkit/bot-slack:test --
src/__tests__/slack-listener.test.ts`
- `pnpm exec nx run @copilotkit/bot-slack:test`
- `pnpm exec nx run @copilotkit/bot-slack:check-types`
- `pnpm exec nx run slack-example:test`
- `pnpm exec nx run slack-example:check-types`
- targeted `pnpm exec oxfmt --check ...`
- targeted `git diff --check ...`
The showcase-aimock RAILWAY.md described fixtures as fetched from GitHub-raw
URLs at boot and called the Dockerfile dead code. Both are false and actively
misled debugging: fixtures are baked into the image at build time under
/fixtures/{shared,d4,d6}, the Dockerfile is the live image builder driven by
showcase_build.yml, and the d5-all.json bundle no longer exists (split into
the per-slug d6/ tree). Corrects sections 3, 4, 5, 6, 7, 8, 9 to match the
real infra, fixes the startCommand to load the three baked-in subdirectories,
and documents the account-scoped RAILWAY_TOKEN mutation path by mechanism.
## Summary
- Moves the canonical `/threads` guide into the **Build Chat UIs** nav
group, immediately after prebuilt components
- Keeps `/premium/threads-explained` under **Intelligence Platform** as
the architecture/persistence explanation
- Adds contextual cross-links between the Threads guide, Threads
architecture page, and relevant prebuilt chat UI docs
- Shows `Threads` in the authored framework sidebars next to their chat
UI basics
## Why
Threads are primarily discovered by developers adding saved
conversations, history, and thread switching to a chat UI. The
implementation guide belongs with chat UI docs, while the platform page
remains the deeper explanation of persistence, realtime sync, and
Enterprise Intelligence Platform backing.
## Screenshots
**Root docs navigation: `/threads` now appears with the chat UI basics,
immediately after Prebuilt Components.**

**Authored framework navigation: framework-specific docs now show
Threads next to Prebuilt Components too.**

**Intelligence Platform navigation: the architecture page stays in the
platform section.**

## Validation
- `git diff --check origin/main...HEAD`
- `git diff --check`
- `npm run typecheck` from `showcase/shell-docs`
- Local route smoke checks for `/threads`, `/premium/threads-explained`,
`/prebuilt-components`, and `/prebuilt-components/chat` returned 200
- Authored framework route smoke checks returned 200
## Summary
- Adds a `thread_persistence_pattern` manifest flag so shared docs can
render selected-framework Threads guidance.
- Marks LangGraph Python, LangGraph TypeScript, LangGraph FastAPI, and
Google ADK with the appropriate thread persistence pattern.
- Extends `WhenFrameworkHas` support so the shared Threads guide can
show LangGraph-only and ADK-only callouts.
- Clarifies that `useThreads` manages Enterprise Intelligence Platform
thread records, not native framework stores.
- Adds framework-selected callouts to the root/shared Threads guide
without adding a third setup path.
## Notes
The new callouts intentionally avoid claiming external store listing,
lifecycle sync, migration/import tooling, or durable ADK sessions by
default. Those remain product/runtime follow-ups tracked separately.
## Validation
- `git diff --check`
- `npm run pretypecheck` in `showcase/shell-docs`
- `npm run lint` in `showcase/shell-docs` (passes with existing
warnings)
- `npm run typecheck` in `showcase/shell-docs`
- `npm run build` in `showcase/shell-docs` (passes with existing
Turbopack/NFT warning)
- Local route smoke checks:
- `/threads` hides framework callouts
- `/langgraph-python/threads` shows LangGraph callout only
- `/langgraph-typescript/threads` shows LangGraph callout only
- `/langgraph-fastapi/threads` shows LangGraph callout only
- `/google-adk/threads` shows ADK callout only
## Summary
- Adds the Inspector Threads locked state for non-Intelligence installs
with a faded full-background thread mockup, updated headline, CTA
order/styling, and production Talk to an Engineer destination.
- Adds Threads telemetry events and metadata for tab click,
locked/enabled/empty views, and CTA clicks, including
package/runtime/status fields that can be derived safely.
- Hardens adjacent inspector paths found during review: telemetry
opt-out and serialization safety, preserved realtime wsUrl on header
refresh, unknown thread endpoint status handling, stale selected-thread
rendering, announcement HTML/link safety, RUN_ERROR styling, context
value rendering, and thread-detail URL joining for trailing-slash
runtime URLs.
## Telemetry notes
Implemented derivable/safe fields: package_name, package_version,
inspector_distinct_id, posthog_distinct_id for attributed CTA URLs,
intelligence_status, thread_service_status, runtime_url_type,
runtime_mode, license_status, telemetry_disabled, cta, cta_surface, and
thread_count.
Telemetry sink allowlist checked: `CopilotKit/oss-path-to-production`
already accepts the owned `oss.inspector.*` namespace via its prefix
gate, so no sink source allowlist change is required. Added
https://github.com/CopilotKit/oss-path-to-production/pull/173 to pin the
new Inspector Threads event names in ingest tests. The package/version
metadata now reads from `packages/web-inspector/package.json` instead of
a hardcoded version string.
Not implemented because they are not currently available from safe
public inspector state:
- has_public_license_key: no explicit public boolean is exposed;
deriving from headers/config could imply token state.
- cta_variant: current UI has no variant state to distinguish.
## Validation
- NX_TUI=false pnpm nx run @copilotkit/web-inspector:check-types
--skip-nx-cache
- NX_TUI=false pnpm nx run @copilotkit/web-inspector:test
--skip-nx-cache (43 tests passed)
- NX_TUI=false pnpm nx run @copilotkit/web-inspector:build
--skip-nx-cache
- NX_TUI=false pnpm nx run @copilotkit/core:check-types --skip-nx-cache
- git diff --check
- Telemetry sink validation in
https://github.com/CopilotKit/oss-path-to-production/pull/173: `pnpm
test -- telemetry-sink-ingest.test.ts` and `pnpm build`
- 7-agent code review pass, followed by 3-agent focused confirmation
pass with no remaining findings in scope
- Pre-commit package matrix hit an unrelated @copilotkit/react-core:test
timeout in `A2UIMessageRenderer.test.tsx` during amend; targeted
web-inspector checks above passed
## Screenshot

## What
Wires the `strands-typescript` showcase integration for **staging**
deployment, mirroring how the Python `strands` integration is deployed.
Provisions the Railway service and adds all repo-side deploy wiring so
the integration appears in the staging integration switcher menu.
## Why
The `strands-typescript` integration code was complete and ran locally,
but was not deployed (`deployed: false`, no Railway service), so it was
absent from the staging integration menu.
## Changes
- **manifest**: `deployed: true` so the shell lists it in the
integration menu.
- **railway-envs.ts**: new `showcase-strands-typescript` SSOT entry.
**Staging-only for now**: the prod instance is not yet provisioned, so
the entry omits the prod env and is `gateIgnore`'d (like
`harness-workers`) until it is promoted dual-env. Regenerated
`railway-envs.generated.json`.
- **showcase_build.yml** + **showcase_build_check.yml**: build-matrix
entry, change-detection filter, and dispatch option. `railway_id` is the
new Railway service id.
- **golden fixture** + **image-ref-gate inventory tests**: updated for
the new service (count 40 → 41, gateIgnore allowlist).
## Railway provisioning (done via CLI/API)
- Service `showcase-strands-typescript` created in the **showcase**
project.
- serviceId `d6f47c8c-a0a1-4dbe-991c-50f8463fd68d`
- staging instanceId `3f917b9f-c3f0-4d8b-96ca-7f455e06b5ba`
- staging domain `showcase-strands-typescript-staging.up.railway.app`,
health `/api/health`, restart ON_FAILURE
- staging vars: `MODEL_PROVIDER=openai`, `MODEL_ID=gpt-4o`,
`OPENAI_BASE_URL=https://aimock-staging.up.railway.app/v1`,
`OPENAI_API_KEY` (placeholder; aimock-fronted), `NODE_ENV=production`,
`NEXT_PUBLIC_BASE_URL=https://showcase.staging.copilotkit.ai`
- No Bedrock/AgentCore creds needed: like the rest of the fleet, LLM
traffic routes through the env-local **aimock** (deterministic replay),
so no real provider key is required.
## Post-merge behavior
On merge to `main`, `showcase_build.yml` builds + pushes
`ghcr.io/copilotkit/showcase-strands-typescript:latest`, redeploys the
staging service, and rebuilds the shell (picking up `deployed: true`).
After that the service is healthy and strands-typescript appears in the
staging integration menu. The image does not exist in GHCR until that
first build, so the Railway service is expected to be unhealthy until
then.
## Prod (follow-up)
Prod is intentionally not provisioned here. It will be added (dual-env,
`gateValidated: true`, `gateIgnore` removed) when the service is
promoted to prod via `showcase_promote.yml`.
## Verification
- `emit-railway-envs-json --check`: up to date
- `sync-promote-service-options --check`: dropdown up to date
(staging-only correctly excluded)
- railway/promote/emit/gate/deploy/registry vitest: 137 passing
- `oxfmt --check`: clean
- `validate-constraints strands-typescript`: valid